diff mbox

[Branch,~linaro-validation/lava-dashboard/trunk] Rev 373: completely redo the ui for attachments and partially redo the run and result pages

Message ID 20121211021813.15470.38405.launchpad@ackee.canonical.com
State Accepted
Headers show

Commit Message

Michael-Doyle Hudson Dec. 11, 2012, 2:18 a.m. UTC
Merge authors:
  Michael Hudson-Doyle (mwhudson)
Related merge proposals:
  https://code.launchpad.net/~mwhudson/lava-dashboard/better-attachment-ui/+merge/138898
  proposed by: Michael Hudson-Doyle (mwhudson)
------------------------------------------------------------
revno: 373 [merge]
committer: Michael Hudson-Doyle <michael.hudson@linaro.org>
branch nick: trunk
timestamp: Tue 2012-12-11 15:17:20 +1300
message:
  completely redo the ui for attachments and partially redo the run and result pages
removed:
  dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html
  dashboard_app/templates/dashboard_app/attachment_detail.html
  dashboard_app/templates/dashboard_app/attachment_list.html
added:
  dashboard_app/static/images/attachment.png
  dashboard_app/static/images/file-icon.png
  dashboard_app/templates/dashboard_app/_attachments.html
  dashboard_app/templates/dashboard_app/attachment_view.html
modified:
  dashboard_app/models.py
  dashboard_app/static/css/dashboard.css
  dashboard_app/templates/dashboard_app/test_result_detail.html
  dashboard_app/templates/dashboard_app/test_run_detail.html
  dashboard_app/urls.py
  dashboard_app/views/__init__.py


--
lp:lava-dashboard
https://code.launchpad.net/~linaro-validation/lava-dashboard/trunk

You are subscribed to branch lp:lava-dashboard.
To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-dashboard/trunk/+edit-subscription
diff mbox

Patch

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2012-11-27 21:54:28 +0000
+++ dashboard_app/models.py	2012-12-11 02:10:26 +0000
@@ -1023,31 +1023,6 @@ 
     def __unicode__(self):
         return self.content_filename
 
-    def get_content_if_possible(self, mirror=False):
-        if self.content:
-            self.content.open()
-            try:
-                data = self.content.read()
-            finally:
-                self.content.close()
-        elif self.public_url and mirror:
-            import urllib
-            stream = urllib.urlopen(self.public_url)
-            try:
-                data = stream.read()
-            except:
-                data = None
-            else:
-                from django.core.files.base import ContentFile
-                self.content.save(
-                    "attachment-{0}.txt".format(self.pk),
-                    ContentFile(data))
-            finally:
-                stream.close()
-        else:
-            data = None
-        return data
-
     def is_test_run_attachment(self):
         if (self.content_type.app_label == 'dashboard_app' and
             self.content_type.model == 'testrun'):
@@ -1076,22 +1051,24 @@ 
             run = self.test_run
         return run.bundle
 
-
-    @models.permalink
-    def get_absolute_url(self):
-        if self.is_test_run_attachment():
-            return ("dashboard_app.views.attachment_detail",
-                    [self.test_run.bundle.bundle_stream.pathname,
-                     self.test_run.bundle.content_sha1,
-                     self.test_run.analyzer_assigned_uuid,
-                     self.pk])
-        elif self.is_test_result_attachment():
-            return ("dashboard_app.views.result_attachment_detail",
-                    [self.test_result.test_run.bundle.bundle_stream.pathname,
-                     self.test_result.test_run.bundle.content_sha1,
-                     self.test_result.test_run.analyzer_assigned_uuid,
-                     self.test_result.relative_index,
-                     self.pk])
+    def get_content_size(self):
+        try:
+            return filesizeformat(self.content.size)
+        except OSError:
+            return "unknown size"
+
+    @models.permalink
+    def get_download_url(self):
+        return ("dashboard_app.views.attachment_download",
+                [self.pk])
+
+    @models.permalink
+    def get_view_url(self):
+        return ("dashboard_app.views.attachment_view",
+                [self.pk])
+
+    def is_viewable(self):
+        return self.mime_type in ['text/plain']
 
 
 class TestResult(models.Model):

=== modified file 'dashboard_app/static/css/dashboard.css'
--- dashboard_app/static/css/dashboard.css	2011-10-13 14:40:48 +0000
+++ dashboard_app/static/css/dashboard.css	2012-12-09 23:45:07 +0000
@@ -4,3 +4,18 @@ 
   margin: 1em auto;
   padding: 1em;
 }
+
+ul.attachments {
+    padding-left: 10px;
+}
+ul.attachments li {
+    background: url("../images/file-icon.png") no-repeat;
+    list-style-type: none;
+    padding-left: 20px;
+}
+
+#lava-sidebar ul.attributes li {
+    padding-left: 0;
+    background: none;
+    list-style-type: none;
+}

=== added file 'dashboard_app/static/images/attachment.png'
Binary files dashboard_app/static/images/attachment.png	1970-01-01 00:00:00 +0000 and dashboard_app/static/images/attachment.png	2012-12-09 22:20:28 +0000 differ
=== added file 'dashboard_app/static/images/file-icon.png'
Binary files dashboard_app/static/images/file-icon.png	1970-01-01 00:00:00 +0000 and dashboard_app/static/images/file-icon.png	2012-12-07 03:09:21 +0000 differ
=== removed file 'dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html'
--- dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html	2011-07-12 02:34:12 +0000
+++ dashboard_app/templates/dashboard_app/_ajax_attachment_viewer.html	1970-01-01 00:00:00 +0000
@@ -1,12 +0,0 @@ 
-{% load i18n %}
-{% if lines %}
-<ol class="file_listing">
-  {% for line in lines %}
-  <li id="L{{forloop.counter}}">
-  <a href="#L{{forloop.counter}}">{{line}}</a>
-  </li>
-  {% endfor %}
-</ol>
-{% else %}
-<h1>{% trans "Viewer not available" %}</h1>
-{% endif %}

=== added file 'dashboard_app/templates/dashboard_app/_attachments.html'
--- dashboard_app/templates/dashboard_app/_attachments.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/_attachments.html	2012-12-10 01:28:50 +0000
@@ -0,0 +1,23 @@ 
+{% if attachments.all %}
+<ul class="attachments">
+  {% for attachment in attachments.all %}
+  <li>
+    <b>{{ attachment }}</b> ({{attachment.mime_type}})
+    {% if attachment.content %}
+    ({{ attachment.get_content_size }})
+    <br />
+    <a href="{{ attachment.get_download_url }}">download</a>
+    {% if attachment.is_viewable %}
+    &nbsp;<a href="{{ attachment.get_view_url }}">view</a>
+    {% endif %}
+    {% endif %}
+    {% if attachment.public_url %}
+    <br />
+    <a href="{{ attachment.public_url }}">{{ attachment.public_url }}</a>
+    {% endif %}
+  </li>
+  {% endfor %}
+</ul>
+{% else %}
+<i>none</i>
+{% endif %}

=== removed file 'dashboard_app/templates/dashboard_app/attachment_detail.html'
--- dashboard_app/templates/dashboard_app/attachment_detail.html	2012-05-07 11:51:01 +0000
+++ dashboard_app/templates/dashboard_app/attachment_detail.html	1970-01-01 00:00:00 +0000
@@ -1,51 +0,0 @@ 
-{% extends "dashboard_app/_content.html" %}
-{% load i18n %}
-{% load humanize %}
-
-
-{% block content %}
-<div id="tabs">
-  <ul>
-    <li><a href="#tab-attachment-information">{% trans "Attachment Information" %}</a></li>
-    <li><a id='inline_viewer_link' href="{% url dashboard_app.views.ajax_attachment_viewer attachment.pk %}">{% trans "Inline Viewer" %}</a></li>
-  </ul>
-  <div id="tab-attachment-information">
-    <dl>
-      <dt>{% trans "Pathname" %}</dt>
-      <dd>{{ attachment.content_filename }}</dd>
-      <dt>{% trans "MIME type"%}</dt>
-      <dd>{{ attachment.mime_type }}</dd>
-      <dt>{% trans "Stored in dashboard" %}</dt>
-      <dd>{{ attachment.content|yesno }}</dd>
-      {% if attachment.content %}
-      <dt>{% trans "File size" %}</dt>
-      <dd>{{ attachment.content.size|filesizeformat }}</dd>
-      {% endif %}
-      <dt>{% trans "Stored on 3rd party server" %}</dt>
-      <dd>{{ attachment.public_url|yesno }}</dd>
-      {% if attachment.public_url %}
-      <dt>{% trans "Public URL" %}</dt>
-      <dd><a href="{{ attachment.public_url }}">{{ attachment.public_url }}</a></dd>
-      {% endif %}
-    </dl>
-  </div>
-</div>
-<script type="text/javascript">
-  $(document).ready(function() {
-    $("#tabs").tabs({
-      ajaxOptions: {
-        dataType: "html",
-        error: function( xhr, status, index, anchor ) {
-          $( anchor.hash ).html(
-          "Couldn't load this tab. We'll try to fix this as soon as possible.");
-        }
-      }
-    });
-    {% ifnotequal attachment.mime_type  "text/plain" %}
-      $('#inline_viewer_link').attr('href', "{% url dashboard_app.views.ajax_attachment_viewer attachment.pk %}");
-      $('#inline_viewer_link').unbind( ".tabs" );
-    {% endifnotequal %}
-
-  });
-</script>
-{% endblock %}

=== removed file 'dashboard_app/templates/dashboard_app/attachment_list.html'
--- dashboard_app/templates/dashboard_app/attachment_list.html	2011-09-16 09:12:11 +0000
+++ dashboard_app/templates/dashboard_app/attachment_list.html	1970-01-01 00:00:00 +0000
@@ -1,46 +0,0 @@ 
-{% extends "dashboard_app/_content.html" %}
-{% load i18n %}
-
-
-{% block content %}
-<table id="attachments" class="demo_jui display">
-  <thead>
-    <tr>
-      <th>Name</th>
-      <th>Size</th>
-      <th>MIME type</th>
-      <th>Public URL</th>
-    </tr>
-  </thead>
-  <tbody>
-    {% for attachment in attachment_list %}
-    <tr>
-      <td><a href="{{ attachment.get_absolute_url }}"
-          ><code>{{ attachment.content_filename }}</code></a></td>
-      <td>
-        {% if attachment.content %}
-        {{ attachment.content.size|filesizeformat }}
-        {% else %}
-        Not available
-        {% endif %}
-      </td>
-      <td><code>{{ attachment.mime_type }}</code></td>
-      <td>
-        {% if attachment.public_url %}
-        <a href="{{ attachment.public_url }}">public URL</a>
-        {% endif %}
-      </td>
-    </tr>
-    {% endfor %}
-  </body>
-</table>
-<script type="text/javascript" charset="utf-8"> 
-  $(document).ready(function() {
-    $('#attachments').dataTable({
-      bJQueryUI: true,
-      bPaginate: false,
-      aaSorting: [[1, "desc"]],
-    });
-  });
-</script>
-{% endblock %}

=== added file 'dashboard_app/templates/dashboard_app/attachment_view.html'
--- dashboard_app/templates/dashboard_app/attachment_view.html	1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/attachment_view.html	2012-12-11 02:14:29 +0000
@@ -0,0 +1,27 @@ 
+{% extends "layouts/base.html" %}
+
+{% block extrahead %}
+<style type="text/css">
+body { background: white; color: black; padding: 0.5em; }
+pre:target { background: rgb(255, 128, 128); }
+pre { margin: 0; }
+p.footer {
+  background: rgba(32, 32, 32);
+  background: rgba(32, 32, 32, 0.8);
+  color: #eee;
+  margin:0;
+  padding: 1em;
+  position: fixed;
+  left: 0;
+  bottom: 0;
+  width: 100%;
+}
+</style>
+{% endblock %}
+
+{% block body %}
+<p class="footer">
+Attachment &ldquo;{{ attachment.content_filename }}&rdquo; of <a href="{{ attachment.content_object.get_absolute_url }}">{{ attachment.content_object }}</a>
+</p>
+{% for line in attachment.content %}<pre id="L{{forloop.counter}}">{{line}}</pre>{% endfor %}
+{% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_result_detail.html'
--- dashboard_app/templates/dashboard_app/test_result_detail.html	2012-11-08 22:56:59 +0000
+++ dashboard_app/templates/dashboard_app/test_result_detail.html	2012-12-11 02:05:33 +0000
@@ -2,23 +2,79 @@ 
 {% load i18n %}
 {% load humanize %}
 
-
-{% block sidebar %}
-<div class="ui-widget">
-  <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em">
-    <span
-      class="ui-icon ui-icon-info"
-      style="float: left; margin-right: 0.3em;"></span>
-    <strong>Note:</strong> This is all the information that the dashboard has
-    about this result. Log analyzers can provide additional information by
-    scrubbing it from the log file.  Information that is global to a test run
-    can be attached to test run attributes instead.
-  </div>
-</div>
-<br/>
+{% block extrahead %}
+{{ block.super }}
+<style type="text/css">
+dt { font-weight: bold }
+dd pre { margin: 0;}
+</style>
+{% endblock %}
+
+{% block content %}
+<h2>Result: {{ test_result.test_case|default:"<i>unknown test case</i>" }}</h2>
+<dl>
+  <dt>{% trans "Test outcome" %}</dt>
+  <dd>
+    <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ test_result.result_code }}.png"
+         alt="{{ test_result.get_result_display }}" width="16" height="16" border="0"/>
+    {{ test_result.get_result_display }}
+  </dd>
+  <dt>{% trans "Measurement" %}</dt>
+  <dd>
+  {% if test_result.measurement != None %}
+    {{ test_result.measurement }} {{ test_result.test_case.units }}
+  {% else %}
+    <i>{% trans "no measurement taken" %}</i>
+  {% endif %}
+  </dd>
+  <dt>{% trans "Log file location" %}</dt>
+  <dd>
+    {% if test_result.filename %}
+        {% if test_result.related_attachment_available and test_result.related_attachment.is_viewable %}
+        {% with test_result.related_attachment as attachment %}
+        <a href="{{ attachment.get_view_url }}">{{ test_result.filename }}</a> line <a href="{{ attachment.get_view_url }}#L{{test_result.lineno}}">{{ test_result.lineno }}</a>
+        {% endwith %}
+        {% else %}
+            {{ test_result.filename }} line {{ test_result.lineno }}
+        {% endif %}
+    {% else %}
+        <i>{% trans "information not provided" %}</i>
+    {% endif %}
+  </dd>
+  <dt>{% trans "Message from the log file" %}</dt>
+  <dd>
+  {% if test_result.message %}
+    <pre>{{ test_result.message }}</pre>
+  {% else %}
+    <i>{% trans "information not provided" %}</i>
+  {% endif %}
+  </dd>
+  <dt>{% trans "Test started on" %}</dt>
+  <dd>
+  {% if test_result.timestamp %}
+    {{ test_result.timestamp|naturalday }}
+    {{ test_result.timestamp|time }}
+  {% else %}
+    <i>{% trans "information not provided" %}</i>
+  {% endif %}
+  </dd>
+  <dt>{% trans "Test duration" %}</dt>
+  <dd>
+  {% if test_result.duration %}
+    {# TODO need a filter for displaying this sensibly. Currently there are some rounding errors #}
+    {{ test_result.duration }}
+  {% else %}
+    <i>{% trans "information not provided" %}</i>
+  {% endif %}
+  </dd>
+</dl>
+
+<h3 id="attachments">Attachments</h3>
+
+{% include "dashboard_app/_attachments.html" with attachments=test_result.attachments %}
+
 {% if test_result.test_run.get_results.count > 1 %}
-<h3>Other results</h3>
-<p class="hint">Results from the same test run are available here</p>
+<h3>Other results from the same test run</h3>
 <select id="other_results">
   {% regroup test_result.test_run.get_results by test_case as test_result_group_list %}
   {% for test_result_group in test_result_group_list %}
@@ -41,129 +97,44 @@ 
   });
 </script>
 {% endif %}
+
 {% endblock %}
 
-
-{% block content %}
-<h2>{% trans "Test Result Details" %}</h2>
+{% block sidebar %}
+<h3>Metadata</h3>
 <dl>
-  <dt>{% trans "Result ID:" %}</dt>
-  <dd>
-  {{ test_result }}
-  <div class="ui-widget" style="width: 30em">
-    <div class="ui-state-highlight ui-corner-all" style="padding: 0.7em">
-      <span
-        class="ui-icon ui-icon-info"
-        style="float: left; margin-right: 0.3em;"></span>
-      <strong>{% trans "Note:" %}</strong>
-      {% blocktrans %}
-      You can navigate to this test result, regardless of the bundle stream it is
-      located in, by using this
-      {% endblocktrans %}
-      <a href="{{ test_result.get_permalink }}" >{% trans "permalink" %}</a>
-    </div>
-  </div>
-  </dd> 
-  <dt>{% trans "Test case:" %}</dt>
-  <dd>
-  {% if test_result.test_case %}
+  <dt>ID</dt>
+  <dd>
+    <small><span style="white-space:nowrap">{{ test_result.test_run.analyzer_assigned_uuid }}/{{ test_result.relative_index }}</span> (<a href="{{ test_result.get_permalink }}">{% trans "permalink" %}</a>)</small>
+  </dd>
+  <dt>Test Case</dt>
+  <dd>
+    {% if test_result.test_case %}
     <b>{{ test_result.test_case }}</b>
-  {% else %}
-  <i>{% trans "unknown test case" %}</i>
-  {% endif %}
-  {% trans "from test" %} <b><a href="{{ test_result.test_run.test.get_absolute_url }}">{{ test_result.test_run.test }}</a></b>
-  </dd>
-  <dt>{% trans "Test outcome:" %}</dt>
-  <dd>{{ test_result.get_result_display }}</dd>
-  <dt>{% trans "Measurement:" %}</dt>
-  <dd>
-  {% if test_result.measurement != None %}
-    {{ test_result.measurement|floatformat }} {{ test_result.test_case.units }}
-  {% else %}
-    <i>{% trans "no measurement taken" %}</i>
-  {% endif %}
-  </dd>
-  <dt>{% trans "Location in the original log file:" %}</dt>
-  <dd>
-    {% if test_result.filename %}
-        {% if test_result.related_attachment_available %}
-        {% with test_result.related_attachment as attachment %}
-        <a href="{{ attachment.get_absolute_url }}">{{ test_result.filename }}</a> line <a href="{{ attachment.get_absolute_url }}#L{{test_result.lineno}}">{{ test_result.lineno }}</a>
-        {% endwith %}
-        {% else %}
-            {{ test_result.filename }} line {{ test_result.lineno }}
-        {% endif %}
     {% else %}
-        <i>{% trans "information not provided" %}</i>
+    <i>{% trans "unknown test case" %}</i>
     {% endif %}
-  </dd>
-  <dt>{% trans "Message from the log file:" %}</dt>
-  <dd>
-  {% if test_result.message %}
-    <code>{{ test_result.message|linebreaks }}</code>
-  {% else %}
-    <i>{% trans "information not provided" %}</i>
-  {% endif %}
-  </dd>
-  <dt>{% trans "Test started on:" %}</dt>
-  <dd>
-  {% if test_result.timestamp %}
-    {{ test_result.timestamp|naturalday }}
-    {{ test_result.timestamp|time }}
-  {% else %}
-    <i>{% trans "information not provided" %}</i>
-  {% endif %}
-  </dd>
-  <dt>{% trans "Test duration:" %}</dt>
-  <dd>
-  {% if test_result.duration %}
-    {# TODO need a filter for displaying this sensibly. Currently there are some rounding errors #}
-    {{ test_result.duration }}
-  {% else %}
-    <i>{% trans "information not provided" %}</i>
-  {% endif %}
-  </dd>
-  <dt>{% trans "Custom attributes (defined at test result level):" %}</dt>
-  <dd>
-  {% with test_result.attributes.values as attrs %}
-  {% if attrs %}
-  <dl>
-    {% for item in attrs|dictsort:"name" %}
-    <dt>{{ item.name|title }}</dt>
-    <dd>{{ item.value }}</dd>
-    {% endfor %}
-  </dl>
-  {% else %}
-  <i>{% trans "none specified" %}</i>
-  {% endif %}
-  {% endwith %}
-  </dd>
-  <dt>{% trans "Custom attributes (defined at test run level):" %}</dt>
-  <dd>
-  {% with test_result.test_run.attributes.values as attrs %}
-  {% if attrs %}
-  <dl>
-    {% for item in attrs|dictsort:"name" %}
-    <dt>{{ item.name|title }}</dt>
-    <dd>{{ item.value }}</dd>
-    {% endfor %}
-  </dl>
-  {% else %}
-  <i>{% trans "none specified" %}</i>
-  {% endif %}
-  {% endwith %}
-  </dd>
-  <dt>
-    Test result attachments
-  </dt>
-  <dd>
-    <ul>
-      {% for attachment in test_result.attachments.all %}
-        <li>
-          <a href="{{ attachment.get_absolute_url }}">{{ attachment }}</a>
-        </li>
-      {% endfor %}
-    </ul>
+    {% trans "from test" %} <b><a href="{{ test_result.test_run.test.get_absolute_url }}">{{ test_result.test_run.test }}</a></b>
   </dd>
 </dl>
+<h3>Result Attributes</h3>
+{% with test_result.attributes.values as attrs %}
+<ul class="attributes">
+  {% for item in attrs|dictsort:"name" %}
+  <li>{{ item.name }}&nbsp;=&nbsp;{{ item.value }}</li>
+  {% empty %}
+  <i>none</i>
+  {% endfor %}
+</ul>
+{% endwith %}
+<h3>Run Attributes</h3>
+{% with test_result.test_run.attributes.values as attrs %}
+<ul class="attributes">
+  {% for item in attrs|dictsort:"name" %}
+  <li>{{ item.name }}&nbsp;=&nbsp;{{ item.value }}</li>
+  {% empty %}
+  <i>none</i>
+  {% endfor %}
+</ul>
+{% endwith %}
 {% endblock %}

=== modified file 'dashboard_app/templates/dashboard_app/test_run_detail.html'
--- dashboard_app/templates/dashboard_app/test_run_detail.html	2012-03-12 05:02:49 +0000
+++ dashboard_app/templates/dashboard_app/test_run_detail.html	2012-12-11 00:55:30 +0000
@@ -7,61 +7,33 @@ 
 
 {% block content %}
 
+<h2>Results</h2>
+
 {% render_table test_table %}
 
+<h3>Attachments</h3>
+
+{% include "dashboard_app/_attachments.html" with attachments=test_run.attachments %}
+
 {% endblock %}
 
 
 {% block sidebar %}
-<h3>Permalink</h3>
-<p>You can navigate to this test run, regardless of the bundle stream it is
-located in, by using this <a
-  href="{% url dashboard_app.views.redirect_to_test_run test_run.analyzer_assigned_uuid %}"
-  >permalink</a>.</p>
-
 <h3>Test run details</h3>
 <dl>
-  <dt>{% trans "Test Name:" %}</dt>
+  <dt>{% trans "Test Name" %} (<abbr title="This is the identifier of the test that was invoked. A test is a collection of test cases. Test is also the smallest piece of code that can be invoked by lava-test.">?</abbr>):</dt>
   <dd><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test.test_id }}</a>
-  <p class="help_text">This is the identifier of the test that was invoked. A
-  test is a collection of test cases. Test is also the smallest piece of code
-  that can be invoked by lava-test.</p>
-  </dd>
-  <dt>{% trans "Test Run UUID:" %}</dt>
-  <dd><small>{{ test_run.analyzer_assigned_uuid }}</small>
-  <p class="help_text">This is a globally unique identifier that was assigned
-  by the log analyzer. Running the same test multiple times results in
-  different values of this identifier.  The dashboard uses this identifier to
-  refer to a particular test run. It is preserved across different LAVA
-  installations, that is, if you pull test results (as bundles) from one system
-  to another this identifier remains intact</p>
-  </dd>
-  <dt>{% trans "Bundle SHA1:" %}</dt>
+  </dd>
+  <dt>{% trans "Test Run UUID" %} (<abbr title="This is a globally unique identifier that was assigned by the log analyzer. Running the same test multiple times results in different values of this identifier.  The dashboard uses this identifier to refer to a particular test run. It is preserved across different LAVA installations, that is, if you pull test results (as bundles) from one system to another this identifier remains intact">?</abbr>):</dt>
+  <dd><small>{{ test_run.analyzer_assigned_uuid }}
+    (<a href="{% url dashboard_app.views.redirect_to_test_run test_run.analyzer_assigned_uuid %}">permalink</a>)</small>
+  </dd>
+  <dt>{% trans "Bundle SHA1" %} (<abbr title="This is the SHA1 hash of the bundle that contains this test run.">?</abbr>):</dt>
   <dd><a href="{{ test_run.bundle.get_absolute_url }}"
     ><small>{{ test_run.bundle.content_sha1 }}</small></a>
-  <p class="help_text">This is the SHA1 hash of the bundle that contains this test run.</p>
-  </dd>
-
-  <dt>{% trans "Attachments:" %}</dt>
-  <dd>
-  <ul>
-    {% for attachment in test_run.attachments.all %}
-    <li><a href="{{ attachment.get_absolute_url }}"
-      >{{ attachment }}</a>
-      {% if attachment.content %}
-      ({{ attachment.content.size|filesizeformat }})
-      {% endif %}
-    </li>
-    {% empty %}
-    <em>{% trans "There are no attachments associated with this test run." %}</em>
-    {% endfor %}
-  </ul>
-  <p class="help_text">LAVA can store attachments associated with a
-  particular test run. Those attachments can be used to store log files, crash
-  dumps, screen shots or other useful test artifacts.</p> 
-  </dd>
-
-  <dt>{% trans "Tags:" %}</dt>
+  </dd>
+
+  <dt>{% trans "Tags" %} (<abbr title="LAVA can store tags associated with a particular test run. Tags are simple strings like &quot;project-foo-prerelase-testing&quot; or &quot;linaro-image-2011-09-27&quot;. Tags can be used by the testing effort feature to group results together.">?</abbr>):</dt>
   <dd>
   <ul>
     {% for tag in test_run.tags.all %}
@@ -70,10 +42,6 @@ 
     <em>{% trans "There are no tags associated with this test run." %}</em>
     {% endfor %}
   </ul>
-  <p class="help_text">LAVA can store tags associated with a particular
-  test run. Tags are simple strings like <q>project-foo-prerelase-testing</q>
-  or <q>linaro-image-2011-09-27</q>. Tags can be used by the testing effort
-  feature to group results together.</p> 
   </dd>
 </dl>
 
@@ -81,20 +49,13 @@ 
 <dl>
   <dt>{% trans "OS Distribution:" %}</dt>
   <dd>{{ test_run.sw_image_desc|default:"<i>Unspecified</i>" }}</dd>
-  <dt>{% trans "Software packages:" %}</dt>
+  <dt>{% trans "Software packages" %} (<abbr title="LAVA keeps track of all the software packages (such as Debian packages managed with dpkg) that were installed prior to running a test. This information can help you track down errors caused by a particular buggy dependency">?</abbr>):</dt>
   <dd><a href="{% url dashboard_app.views.test_run_software_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
     >See all {{ test_run.packages.all.count }} software packages</a>
-  <p class="help_text">LAVA keeps track of all the software packages (such as
-  Debian packages managed with dpkg) that were installed prior to running a
-  test. This information can help you track down errors caused by a particular
-  buggy dependency</p>
   </dd>
-  <dt>{% trans "Software sources:" %}</dt>
+  <dt>{% trans "Software sources" %} (<abbr title="LAVA can track more data than just package name and version. You can track precise software information such as the version control system branch or repository, revision or tag name and more.">?</abbr>):</dt>
   <dd><a href="{% url dashboard_app.views.test_run_software_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
     >See all {{ test_run.sources.all.count }} source references</a>
-  <p class="help_text">LAVA can track more data than just package name and
-  version. You can track precise software information such as the version
-  control system branch or repository, revision or tag name and more</p>
   </dd>
 </dl>
 
@@ -102,63 +63,44 @@ 
 <dl>
   <dt>{% trans "Board:" %}</dt>
   <dd>{{ test_run.get_board|default_if_none:"There are no boards associated with this test run" }}</dd>
-  <dt>{% trans "Other devices:" %}</dt>
-  <dd><a 
+  <dt>{% trans "Other devices" %} (<abbr title="LAVA keeps track of the hardware that was used for testing. This can help cross-reference benchmarks and identify hardware-specific issues.">?</abbr>):</dt>
+  <dd><a
     href="{% url dashboard_app.views.test_run_hardware_context test_run.bundle.bundle_stream.pathname test_run.bundle.content_sha1 test_run.analyzer_assigned_uuid %}"
     >See all {{ test_run.devices.all.count }} devices</a>
-  <p class="help_text">LAVA keeps track of the hardware that was used for
-  testing. This can help cross-reference benchmarks and identify
-  hardware-specific issues.</p>
   </dd>
 </dl>
 
-<h3>Custom attributes</h3>
-<p class="help_text">LAVA can store arbitrary key-value attributes associated
-with each test run (and separately, each test result)</p>
-<ul>
+<h3>Custom attributes (<abbr title="LAVA can store arbitrary key-value attributes associated with each test run (and separately, each test result)">?</abbr>)</h3>
+<ul class="attributes">
   {% for attribute in test_run.attributes.all %}
   <li>{{ attribute.name }} = {{ attribute.value }}</li>
   {% empty %}
-  <em>{% trans "There are no attributes associated with this test run." %}</em>
+  <i>none</i>
   {% endfor %}
 </ul>
 
-<h3>Time stamps</h3>
-<p class="help_text">There are three different timestamps
-associated with each test run. They are explained below.</p>
+<h3>Time stamps (<abbr title="There are three different timestamps associated with each test run. They are explained below.">?</abbr>)</h3>
 <dl>
-  <dt>{% trans "Log analyzed on:" %}</dt>
+  <dt>{% trans "Log analyzed on" %} (<abbr title="This is the moment this that this test run's artifacts (such as log files and other output) were processed by the log analyzer. Typically the analyzer is a part of lava-test framework and test output is analyzed on right on the device so this time may not be trusted, see below for the description of &quot;time check performed&quot;">?</abbr>):</dt>
   <dd>
   {{ test_run.analyzer_assigned_date|naturalday }}
   {{ test_run.analyzer_assigned_date|time }}
   ({{ test_run.analyzer_assigned_date|timesince }} ago)
-  <p class="help_text">This is the moment this that this test run's artifacts
-  (such as log files and other output) were processed by the log analyzer.
-  Typically the analyzer is a part of lava-test framework and test output is
-  analyzed on right on the device so this time may not be trusted, see below
-  for the description of <q>time check performed</q></p>
   </dd>
-  <dt>{% trans "Time check performed" %}</dt>
+  <dt>{% trans "Time check performed" %} (<abbr title="The value &quot;no&quot; indicates that the log analyzer was not certain that the time and date is accurate.">?</abbr>):</dt>
   <dd>{{ test_run.time_check_performed|yesno }}
-  <p class="help_text">The value <em>no</em> indicates that the log analyzer
-  was not certain that the time and date is accurate.</p>
   </dd>
-  <dt>{% trans "Data imported on:" %}</dt>
+  <dt>{% trans "Data imported on" %} (<abbr title="This is the moment this test run entry was created in the LAVA database. It can differ from upload date if there were any initial deserialization problems and the data was deserialized later.">?</abbr>):</dt>
   <dd>
   {{ test_run.import_assigned_date|naturalday }}
   {{ test_run.import_assigned_date|time }}
   ({{ test_run.import_assigned_date|timesince }} ago)
-  <p class="help_text">This is the moment this test run entry was created in
-  the LAVA database. It can differ from upload date if there were any initial
-  deserialization problems and the data was deserialized later.</p>
   </dd>
-  <dt>{% trans "Data uploaded on:" %}</dt>
+  <dt>{% trans "Data uploaded on" %} (<abbr title="This is the moment this data was first uploaded to LAVA (as a serialized bundle).">?</abbr>):</dt>
   <dd>
   {{ test_run.bundle.uploaded_on|naturalday }}
   {{ test_run.bundle.uploaded_on|time }}
   ({{ test_run.bundle.uploaded_on|timesince }} ago)
-  <p class="help_text">This is the moment this data was first uploaded to LAVA
-  (as a serialized bundle).</p>
   </dd>
 </dl>
 {% endblock %}

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2012-11-08 23:10:05 +0000
+++ dashboard_app/urls.py	2012-12-11 02:01:37 +0000
@@ -28,7 +28,6 @@ 
     'dashboard_app.views',
     url(r'^$', 'index'),
     url(r'^ajax/bundle-viewer/(?P<pk>[0-9]+)/$', 'ajax_bundle_viewer'),
-    url(r'^ajax/attachment-viewer/(?P<pk>[0-9]+)/$', 'ajax_attachment_viewer'),
     url(r'^data-views/$', 'data_view_list'),
     url(r'^data-views/(?P<name>[a-zA-Z0-9-_]+)/$', 'data_view_detail'),
     url(r'^reports/$', 'report_list'),
@@ -64,15 +63,13 @@ 
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/json$', 'bundle_json'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'test_run_detail'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/json$', 'test_run_detail_test_json'),
-    url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/attachments$', 'attachment_list'),
-    url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/attachments/(?P<pk>[0-9]+)/$', 'attachment_detail'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/result/(?P<relative_index>[0-9]+)/$', 'test_result_detail'),
-    url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/result/(?P<relative_index>[0-9]+)/attachments$', 'result_attachment_list'),
-    url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/result/(?P<relative_index>[0-9]+)/attachment/(?P<pk>[0-9]+)$', 'result_attachment_detail'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/hardware-context/$', 'test_run_hardware_context'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/software-context/$', 'test_run_software_context'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)test-runs/$', 'test_run_list'),
     url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)test-runs/json$', 'test_run_list_json'),
+    url(r'^attachment/(?P<pk>[0-9]+)/download$', 'attachment_download'),
+    url(r'^attachment/(?P<pk>[0-9]+)/view$', 'attachment_view'),
     url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'redirect_to_test_run'),
     url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<trailing>.*)$', 'redirect_to_test_run'),
     url(r'^permalink/test-result/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<relative_index>[0-9]+)/$', 'redirect_to_test_result'),

=== modified file 'dashboard_app/views/__init__.py'
--- dashboard_app/views/__init__.py	2012-11-16 01:01:22 +0000
+++ dashboard_app/views/__init__.py	2012-12-11 02:01:37 +0000
@@ -29,9 +29,16 @@ 
 from django.core.urlresolvers import reverse
 from django.db.models.manager import Manager
 from django.db.models.query import QuerySet
-from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.db.models import Count
+from django.http import (
+    Http404,
+    HttpResponse,
+    HttpResponseBadRequest,
+    HttpResponseRedirect,
+    )
 from django.shortcuts import render_to_response, redirect, get_object_or_404
 from django.template import RequestContext, loader
+from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.views.generic.list_detail import object_list, object_detail
 
@@ -415,7 +422,14 @@ 
         <a href="{{record.get_absolute_url}}">
           <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.result_code }}.png"
           alt="{{ record.get_result_display }}" width="16" height="16" border="0"/></a>
-        <a href ="{{record.get_absolute_url}}">{{ record.get_result_display }}</a>
+        <a href="{{record.get_absolute_url}}">{{ record.get_result_display }}</a>
+        {% if record.attachments__count %}
+        <a href="{{record.get_absolute_url}}#attachments">
+          <img style="float:right;" src="{{ STATIC_URL }}dashboard_app/images/attachment.png"
+               alt="This result has {{ record.attachments__count }} attachments"
+               title="This result has {{ record.attachments__count }} attachments"
+               /></a>
+        {% endif %}
         ''')
 
     units = TemplateColumn(
@@ -423,10 +437,11 @@ 
         verbose_name="measurement")
 
     def get_queryset(self, test_run):
-        return test_run.get_results()
+        return test_run.get_results().annotate(Count("attachments"))
 
     datatable_opts = {
         'sPaginationType': "full_numbers",
+        'iDisplayLength': 25,
         }
 
     searchable_columns = ['test_case__test_case_id']
@@ -527,7 +542,7 @@ 
         request.user,
         analyzer_assigned_uuid=analyzer_assigned_uuid
     )
-    test_result = test_run.test_results.get(relative_index=relative_index)
+    test_result = test_run.test_results.select_related('fig').get(relative_index=relative_index)
     return render_to_response(
         "dashboard_app/test_result_detail.html", {
             'bread_crumb_trail': BreadCrumbTrail.leading_to(
@@ -540,127 +555,36 @@ 
         }, RequestContext(request))
 
 
-@BreadCrumb(
-    "Attachments",
-    parent=test_run_detail,
-    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid'])
-def attachment_list(request, pathname, content_sha1, analyzer_assigned_uuid):
-    test_run = get_restricted_object(
-        TestRun,
-        lambda test_run: test_run.bundle.bundle_stream,
-        request.user,
-        analyzer_assigned_uuid=analyzer_assigned_uuid
-    )
-    return object_list(
-        request,
-        queryset=test_run.attachments.all(),
-        template_name="dashboard_app/attachment_list.html",
-        template_object_name="attachment",
-        extra_context={
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                attachment_list,
-                pathname=pathname,
-                content_sha1=content_sha1,
-                analyzer_assigned_uuid=analyzer_assigned_uuid),
-            'test_run': test_run})
-
-@BreadCrumb(
-    "Attachments",
-    parent=test_result_detail,
-    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'relative_index'])
-def result_attachment_list(request, pathname, content_sha1, analyzer_assigned_uuid, relative_index):
-    test_result = get_restricted_object(
-        TestResult,
-        lambda test_result: test_result.test_run.bundle.bundle_stream,
-        request.user,
-        test_run__analyzer_assigned_uuid=analyzer_assigned_uuid,
-        relative_index=relative_index,
-    )
-    return object_list(
-        request,
-        queryset=test_result.attachments.all(),
-        template_name="dashboard_app/attachment_list.html",
-        template_object_name="attachment",
-        extra_context={
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                result_attachment_list,
-                pathname=pathname,
-                content_sha1=content_sha1,
-                analyzer_assigned_uuid=analyzer_assigned_uuid,
-                relative_index=relative_index)
-                })
-
-@BreadCrumb(
-    "{content_filename}",
-    parent=attachment_list,
-    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'pk'])
-def attachment_detail(request, pathname, content_sha1, analyzer_assigned_uuid, pk):
-    attachment = get_restricted_object(
-        Attachment,
-        lambda attachment: attachment.bundle.bundle_stream,
-        request.user,
-        pk = pk
-    )
-    return render_to_response(
-        "dashboard_app/attachment_detail.html", {
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                attachment_detail,
-                pathname=pathname,
-                content_sha1=content_sha1,
-                analyzer_assigned_uuid=analyzer_assigned_uuid,
-                pk=pk,
-                content_filename=attachment.content_filename),
-            "attachment": attachment,
-        }, RequestContext(request))
-
-
-@BreadCrumb(
-    "{content_filename}",
-    parent=result_attachment_list,
-    needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'relative_index', 'pk'])
-def result_attachment_detail(request, pathname, content_sha1, analyzer_assigned_uuid, relative_index, pk):
-    attachment = get_restricted_object(
-        Attachment,
-        lambda attachment: attachment.bundle.bundle_stream,
-        request.user,
-        pk = pk
-    )
-    return render_to_response(
-        "dashboard_app/attachment_detail.html", {
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                result_attachment_detail,
-                pathname=pathname,
-                content_sha1=content_sha1,
-                analyzer_assigned_uuid=analyzer_assigned_uuid,
-                relative_index=relative_index,
-                pk=pk,
-                content_filename=attachment.content_filename),
-            "attachment": attachment,
-        }, RequestContext(request))
-
-
-def ajax_attachment_viewer(request, pk):
-    attachment = get_restricted_object(
-        Attachment,
-        lambda attachment: attachment.bundle.bundle_stream,
-        request.user,
-        pk=pk
-    )
-    data = attachment.get_content_if_possible(
-        mirror=request.user.is_authenticated())
-    if attachment.mime_type == "text/plain":
-        return render_to_response(
-            "dashboard_app/_ajax_attachment_viewer.html", {
-                "attachment": attachment,
-                "lines": data.splitlines() if data else None,
-            },
-            RequestContext(request))
-    else:
-        response = HttpResponse(mimetype=attachment.mime_type)
-        response['Content-Disposition'] = 'attachment; filename=%s' % (
-                                           attachment.content_filename)
-        response.write(data)
-        return response
+def attachment_download(request, pk):
+    attachment = get_restricted_object(
+        Attachment,
+        lambda attachment: attachment.bundle.bundle_stream,
+        request.user,
+        pk = pk
+    )
+    if not attachment.content:
+        return HttpResponseBadRequest(
+            "Attachment %s not present on dashboard" % pk)
+    response = HttpResponse(mimetype=attachment.mime_type)
+    response['Content-Disposition'] = 'attachment; filename=%s' % (
+                                       attachment.content_filename)
+    response.write(attachment.content.read())
+    return response
+
+
+def attachment_view(request, pk):
+    attachment = get_restricted_object(
+        Attachment,
+        lambda attachment: attachment.bundle.bundle_stream,
+        request.user,
+        pk = pk
+    )
+    if not attachment.content or not attachment.is_viewable():
+        return HttpResponseBadRequest("Attachment %s not viewable" % pk)
+    return render_to_response(
+        "dashboard_app/attachment_view.html", {
+            'attachment': attachment,
+        }, RequestContext(request))
 
 
 @BreadCrumb("Reports", parent=index)