Merge lp:~mwhudson/lava-dashboard/compare-testrun-view into lp:lava-dashboard

Proposed by Michael Hudson-Doyle
Status: Merged
Merged at revision: 385
Proposed branch: lp:~mwhudson/lava-dashboard/compare-testrun-view
Merge into: lp:lava-dashboard
Diff against target: 569 lines (+439/-3)
9 files modified
dashboard_app/filters.py (+16/-0)
dashboard_app/static/css/filter-detail.css (+52/-0)
dashboard_app/static/js/filter-detail.js (+135/-0)
dashboard_app/templates/dashboard_app/filter_compare_matches.html (+28/-0)
dashboard_app/templates/dashboard_app/filter_detail.html (+19/-0)
dashboard_app/urls.py (+2/-1)
dashboard_app/views/__init__.py (+1/-1)
dashboard_app/views/filters/tables.py (+35/-0)
dashboard_app/views/filters/views.py (+151/-1)
To merge this branch: bzr merge lp:~mwhudson/lava-dashboard/compare-testrun-view
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Review via email: mp+142421@code.launchpad.net

Description of the change

This branch adds a way to compare matches of a filter. You can play with it at http://staging.validation.linaro.org/dashboard/filters/~mwhudson/pmqa-vexpress -- click the compare builds button at the bottom and go from there.

The js stuff to access the page is slightly hackish, but it seems to work for me...

To post a comment you must log in.
402. By Michael Hudson-Doyle

merge trunk

403. By Michael Hudson-Doyle

start to make compare builds selection a bit more ordinary

404. By Michael Hudson-Doyle

finish that

405. By Michael Hudson-Doyle

one more

406. By Michael Hudson-Doyle

fixes++

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I've tried to make the way of getting to the comparison page a little less odd. Let me know what you think.

407. By Michael Hudson-Doyle

move js and css to separate files

408. By Michael Hudson-Doyle

css tweak

Revision history for this message
Andy Doan (doanac) wrote :

I think this works. You've now passed your job interview question with this algorithm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'dashboard_app/filters.py'
--- dashboard_app/filters.py 2012-12-18 00:47:08 +0000
+++ dashboard_app/filters.py 2013-01-10 20:39:21 +0000
@@ -300,6 +300,22 @@
300 q = self.queryset.filter(bundle__uploaded_on__gt=since)300 q = self.queryset.filter(bundle__uploaded_on__gt=since)
301 return self._wrap(q)301 return self._wrap(q)
302302
303 def with_tags(self, tag1, tag2):
304 if self.key == 'build_number':
305 q = self.queryset.extra(
306 where=['convert_to_integer("dashboard_app_namedattribute"."value") in (%s, %s)' % (tag1, tag2)]
307 )
308 else:
309 tag1 = datetime.datetime.strptime(tag1, "%Y-%m-%d %H:%M:%S.%f")
310 tag2 = datetime.datetime.strptime(tag2, "%Y-%m-%d %H:%M:%S.%f")
311 q = self.queryset.filter(bundle__uploaded_on__in=(tag1, tag2))
312 matches = list(self._wrap(q))
313 if matches[0].tag == tag1:
314 return matches
315 else:
316 matches.reverse()
317 return matches
318
303 def count(self):319 def count(self):
304 return self.queryset.count()320 return self.queryset.count()
305321
306322
=== added file 'dashboard_app/static/css/filter-detail.css'
--- dashboard_app/static/css/filter-detail.css 1970-01-01 00:00:00 +0000
+++ dashboard_app/static/css/filter-detail.css 2013-01-10 20:39:21 +0000
@@ -0,0 +1,52 @@
1table.select-compare1 td { cursor: pointer; }
2table.select-compare1 tr.even td {
3 background-color: #ccf;
4}
5table.select-compare1 tr.even.hover td {
6 background-color: #77f;
7}
8table.select-compare1 tr.odd td {
9 background-color: #aaf;
10}
11table.select-compare1 tr.odd.hover td {
12 background-color: #77f;
13}
14
15table.select-compare2 td { cursor: pointer; }
16table.select-compare2 tr.even td {
17 background-color: #fcc;
18}
19table.select-compare2 tr.odd td {
20 background-color: #faa;
21}
22table.select-compare2 tr.selected-1 td {
23 background-color: #77f;
24}
25table.select-compare2 tr.selected-1.hover td {
26 background-color: #77f;
27}
28table.select-compare2 tr.hover td {
29 background-color: #f77;
30}
31table.select-compare3 tr.selected-1 td {
32 background-color: #77f;
33}
34table.select-compare3 tr.selected-1.hover td {
35 background-color: #77f;
36}
37table.select-compare3 tr.selected-2 td {
38 background-color: #f77;
39}
40table.select-compare3 tr.selected-2.hover td {
41 background-color: #f77;
42}
43table.select-compare3 tr.selected-1 {
44 cursor: pointer;
45}
46table.select-compare3 tr.selected-2 {
47 cursor: pointer;
48}
49#filter-table input {
50 margin-top: 0;
51 margin-bottom: 0;
52}
0\ No newline at end of file53\ No newline at end of file
154
=== added file 'dashboard_app/static/js/filter-detail.js'
--- dashboard_app/static/js/filter-detail.js 1970-01-01 00:00:00 +0000
+++ dashboard_app/static/js/filter-detail.js 2013-01-10 20:39:21 +0000
@@ -0,0 +1,135 @@
1var compareState = 0;
2var compare1 = null, compare2 = null;
3function cancelCompare () {
4 $("#filter-table").removeClass("select-compare1");
5 $("#filter-table").removeClass("select-compare2");
6 $("#filter-table").removeClass("select-compare3");
7 $("#filter-table tr").removeClass("selected-1");
8 $("#filter-table tr").removeClass("selected-2");
9 $("#filter-table tr").unbind("click");
10 $("#filter-table tr").unbind("hover");
11 $("#filter-table tr").each(removeCheckbox);
12 $("#first-prompt").hide();
13 $("#second-prompt").hide();
14 $("#third-prompt").hide();
15 $("#compare-button").button({label:"Compare builds"});
16 compareState = 0;
17}
18function startCompare () {
19 $("#compare-button").button({label:"Cancel"});
20 $("#filter-table").addClass("select-compare1");
21 $("#filter-table tr").click(rowClickHandler);
22 $("#filter-table tr").each(insertCheckbox);
23 $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut);
24 $("#first-prompt").show();
25 compareState = 1;
26}
27function tagFromRow(tr) {
28 var firstCell = $(tr).find("td:eq(0)");
29 return {
30 machinetag: firstCell.find("span").data("machinetag"),
31 usertag: firstCell.text()
32 };
33}
34function rowClickHandler() {
35 if (compareState == 1) {
36 compare1 = tagFromRow($(this));
37 $(this).addClass("selected-1");
38 $(this).find("input").attr("checked", true);
39 $("#p2-build").text(compare1.usertag);
40 $("#first-prompt").hide();
41 $("#second-prompt").show();
42 $("#filter-table").removeClass("select-compare1");
43 $("#filter-table").addClass("select-compare2");
44 compareState = 2;
45 } else if (compareState == 2) {
46 var thistag = tagFromRow($(this));
47 if (compare1.machinetag == thistag.machinetag) {
48 cancelCompare();
49 startCompare();
50 } else {
51 compare2 = thistag;
52 $(this).find("input").attr("checked", true);
53 $(this).addClass("selected-2");
54 $("#second-prompt").hide();
55 $("#third-prompt").show();
56 $("#filter-table").removeClass("select-compare2");
57 $("#filter-table").addClass("select-compare3");
58 $("#filter-table input").attr("disabled", true);
59 $("#filter-table .selected-1 input").attr("disabled", false);
60 $("#filter-table .selected-2 input").attr("disabled", false);
61 $("#p3-build-1").text(compare1.usertag);
62 $("#p3-build-2").text(compare2.usertag);
63 $("#third-prompt a").attr("href", window.location + '/+compare/' + compare1.machinetag + '/' + compare2.machinetag);
64 compareState = 3;
65 }
66 } else if (compareState == 3) {
67 var thistag = tagFromRow($(this));
68 if (thistag.machinetag == compare1.machinetag || thistag.machinetag == compare2.machinetag) {
69 $("#second-prompt").show();
70 $("#third-prompt").hide();
71 $("#filter-table").addClass("select-compare2");
72 $("#filter-table").removeClass("select-compare3");
73 $("#filter-table input").attr("disabled", false);
74 compareState = 2;
75 $(this).find("input").attr("checked", false);
76 if (thistag.machinetag == compare1.machinetag) {
77 compare1 = compare2;
78 $("#filter-table .selected-1").removeClass("selected-1");
79 $("#filter-table .selected-2").addClass("selected-1");
80 $("#p2-build").text(compare1.usertag);
81 }
82 $("#filter-table .selected-2").removeClass("selected-2");
83 }
84 }
85 tagFromRow(this);
86}
87function rowHoverHandlerIn() {
88 $(this).addClass("hover");
89}
90function rowHoverHandlerOut() {
91 $(this).removeClass("hover");
92}
93function insertCheckbox() {
94 var row = $(this);
95 var checkbox = $('<input type="checkbox">');
96 row.find("td:first").prepend(checkbox);
97}
98function removeCheckbox() {
99 var row = $(this);
100 row.find('input').remove();
101}
102$(window).load(
103 function () {
104 $("#filter-table").dataTable().fnSettings().fnRowCallback = function(tr, data, index) {
105 if (compareState) {
106 insertCheckbox.call(tr);
107 $(tr).click(rowClickHandler);
108 $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut);
109 if (compareState >= 2 && tagFromRow(tr).machinetag == compare1.machinetag) {
110 $(tr).addClass("selected-1");
111 $(tr).find("input").attr("checked", true);
112 }
113 if (compareState >= 3) {
114 if (tagFromRow(tr).machinetag == compare2.machinetag) {
115 $(tr).addClass("selected-2");
116 $(tr).find("input").attr("checked", true);
117 } else if (tagFromRow(tr).machinetag != compare1.machinetag) {
118 $(tr).find("input").attr("disabled", true);
119 }
120 }
121 }
122 return tr;
123 };
124 $("#compare-button").button();
125 $("#compare-button").click(
126 function (e) {
127 if (compareState == 0) {
128 startCompare();
129 } else {
130 cancelCompare();
131 }
132 }
133 );
134 }
135);
0136
=== added file 'dashboard_app/templates/dashboard_app/filter_compare_matches.html'
--- dashboard_app/templates/dashboard_app/filter_compare_matches.html 1970-01-01 00:00:00 +0000
+++ dashboard_app/templates/dashboard_app/filter_compare_matches.html 2013-01-10 20:39:21 +0000
@@ -0,0 +1,28 @@
1{% extends "dashboard_app/_content.html" %}
2
3{% load django_tables2 %}
4
5{% block extrahead %}
6{{ block.super }}
7<style type="text/css">
8 th.orderable.sortable a {
9 color: rgb(0, 136, 204);
10 text-decoration: underline;
11 }
12</style>
13{% endblock %}
14
15{% block content %}
16{% for trinfo in test_run_info %}
17<h3>{{ trinfo.key }} results</h3>
18{% if trinfo.only %}
19<p style="text-align: {{ trinfo.only }}">
20 Results were only present in <a href="{{ trinfo.tr.get_absolute_url }}">build {{ trinfo.tag }}</a>.
21</p>
22{% elif trinfo.table %}
23{% render_table trinfo.table %}
24{% else %}
25<p>No difference in {{ trinfo.key }} results{% if trinfo.cases %} for {{ trinfo.cases }}{% endif %}.</p>
26{% endif %}
27{% endfor %}
28{% endblock %}
029
=== modified file 'dashboard_app/templates/dashboard_app/filter_detail.html'
--- dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-09 00:05:14 +0000
+++ dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-10 20:39:21 +0000
@@ -2,6 +2,12 @@
2{% load i18n %}2{% load i18n %}
3{% load django_tables2 %}3{% load django_tables2 %}
44
5{% block extrahead %}
6{{ block.super }}
7<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-detail.css"/>
8<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-detail.js"></script>
9{% endblock %}
10
5{% block content %}11{% block content %}
612
7<h1>Filter {{ filter.name }}</h1>13<h1>Filter {{ filter.name }}</h1>
@@ -27,4 +33,17 @@
2733
28{% render_table filter_table %}34{% render_table filter_table %}
2935
36<p>
37 <button id="compare-button">Compare builds</button>
38 <span id="first-prompt" style="display:none">
39 Click a build to compare.
40 </span>
41 <span id="second-prompt" style="display:none">
42 Click build to compare with build <span id="p2-build">XXX</span>.
43 </span>
44 <span id="third-prompt" style="display:none">
45 Click <a href="#">here</a> to compare with build <span id="p3-build-1">XXX</span> with build <span id="p3-build-2">XXX</span>.
46 </span>
47</p>
48
30{% endblock %}49{% endblock %}
3150
=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py 2012-12-11 02:01:37 +0000
+++ dashboard_app/urls.py 2013-01-10 20:39:21 +0000
@@ -45,7 +45,8 @@
45 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filters.views.filter_edit'),45 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filters.views.filter_edit'),
46 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filters.views.filter_subscribe'),46 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filters.views.filter_subscribe'),
47 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filters.views.filter_delete'),47 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filters.views.filter_delete'),
48 url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, 48 url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+compare/(?P<tag1>[a-zA-Z0-9-_: .]+)/(?P<tag2>[a-zA-Z0-9-_: .]+)$', 'filters.views.compare_matches'),
49 url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler,
49 name='dashboard_app.views.dashboard_xml_rpc_handler',50 name='dashboard_app.views.dashboard_xml_rpc_handler',
50 kwargs={51 kwargs={
51 'mapper': legacy_mapper,52 'mapper': legacy_mapper,
5253
=== modified file 'dashboard_app/views/__init__.py'
--- dashboard_app/views/__init__.py 2012-12-17 00:10:53 +0000
+++ dashboard_app/views/__init__.py 2013-01-10 20:39:21 +0000
@@ -38,7 +38,6 @@
38 )38 )
39from django.shortcuts import render_to_response, redirect, get_object_or_40439from django.shortcuts import render_to_response, redirect, get_object_or_404
40from django.template import RequestContext, loader40from django.template import RequestContext, loader
41from django.utils.html import escape
42from django.utils.safestring import mark_safe41from django.utils.safestring import mark_safe
43from django.views.generic.list_detail import object_list, object_detail42from django.views.generic.list_detail import object_list, object_detail
4443
@@ -825,3 +824,4 @@
825 pk=effort.pk)824 pk=effort.pk)
826 })825 })
827 return HttpResponse(t.render(c))826 return HttpResponse(t.render(c))
827
828828
=== modified file 'dashboard_app/views/filters/tables.py'
--- dashboard_app/views/filters/tables.py 2012-12-13 00:51:07 +0000
+++ dashboard_app/views/filters/tables.py 2013-01-10 20:39:21 +0000
@@ -16,8 +16,11 @@
16# You should have received a copy of the GNU Affero General Public License16# You should have received a copy of the GNU Affero General Public License
17# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.17# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
1818
19import datetime
19import operator20import operator
2021
22from django.conf import settings
23from django.template import defaultfilters
21from django.utils.html import escape24from django.utils.html import escape
22from django.utils.safestring import mark_safe25from django.utils.safestring import mark_safe
2326
@@ -184,6 +187,12 @@
184 self.base_columns.insert(0, 'bundle_stream', bundle_stream_col)187 self.base_columns.insert(0, 'bundle_stream', bundle_stream_col)
185 self.base_columns.insert(0, 'tag', tag_col)188 self.base_columns.insert(0, 'tag', tag_col)
186189
190 def render_tag(self, value):
191 if isinstance(value, datetime.datetime):
192 strvalue = defaultfilters.date(value, settings.DATETIME_FORMAT)
193 else:
194 strvalue = value
195 return mark_safe('<span data-machinetag="%s">%s</span>' % (escape(str(value)), strvalue))
187 tag = Column()196 tag = Column()
188197
189 def render_bundle_stream(self, record):198 def render_bundle_stream(self, record):
@@ -225,3 +234,29 @@
225 datatable_opts.update({234 datatable_opts.update({
226 "iDisplayLength": 10,235 "iDisplayLength": 10,
227 })236 })
237
238
239class TestResultDifferenceTable(DataTablesTable):
240 test_case_id = Column(verbose_name=mark_safe('test_case_id'))
241 first_result = TemplateColumn('''
242 {% if record.first_result %}
243 <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.first_result }}.png"
244 alt="{{ record.first_result }}" width="16" height="16" border="0"/>{{ record.first_result }}
245 {% else %}
246 <i>missing</i>
247 {% endif %}
248 ''')
249 second_result = TemplateColumn('''
250 {% if record.second_result %}
251 <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.second_result }}.png"
252 alt="{{ record.second_result }}" width="16" height="16" border="0"/>{{ record.second_result }}
253 {% else %}
254 <i>missing</i>
255 {% endif %}
256 ''')
257
258 datatable_opts = {
259 'iDisplayLength': 25,
260 'sPaginationType': "full_numbers",
261 }
262
228263
=== modified file 'dashboard_app/views/filters/views.py'
--- dashboard_app/views/filters/views.py 2012-11-27 05:07:00 +0000
+++ dashboard_app/views/filters/views.py 2013-01-10 20:39:21 +0000
@@ -25,12 +25,17 @@
25from django.http import HttpResponse, HttpResponseRedirect25from django.http import HttpResponse, HttpResponseRedirect
26from django.shortcuts import render_to_response26from django.shortcuts import render_to_response
27from django.template import RequestContext27from django.template import RequestContext
28from django.utils.html import escape
29from django.utils.safestring import mark_safe
2830
29from lava_server.bread_crumbs import (31from lava_server.bread_crumbs import (
30 BreadCrumb,32 BreadCrumb,
31 BreadCrumbTrail,33 BreadCrumbTrail,
32)34)
3335
36from dashboard_app.filters import (
37 evaluate_filter,
38 )
34from dashboard_app.models import (39from dashboard_app.models import (
35 NamedAttribute,40 NamedAttribute,
36 Test,41 Test,
@@ -39,7 +44,9 @@
39 TestRunFilter,44 TestRunFilter,
40 TestRunFilterSubscription,45 TestRunFilterSubscription,
41 )46 )
42from dashboard_app.views import index47from dashboard_app.views import (
48 index,
49 )
43from dashboard_app.views.filters.forms import (50from dashboard_app.views.filters.forms import (
44 TestRunFilterForm,51 TestRunFilterForm,
45 TestRunFilterSubscriptionForm,52 TestRunFilterSubscriptionForm,
@@ -48,6 +55,7 @@
48 FilterTable,55 FilterTable,
49 FilterPreviewTable,56 FilterPreviewTable,
50 PublicFiltersTable,57 PublicFiltersTable,
58 TestResultDifferenceTable,
51 UserFiltersTable,59 UserFiltersTable,
52 )60 )
5361
@@ -248,3 +256,145 @@
248 json.dumps(list(result)),256 json.dumps(list(result)),
249 mimetype='application/json')257 mimetype='application/json')
250258
259
260def _iter_matching(seq1, seq2, key):
261 """Iterate over sequences in the order given by the key function, matching
262 elements with matching key values.
263
264 For example:
265
266 >>> seq1 = [(1, 2), (2, 3)]
267 >>> seq2 = [(1, 3), (3, 4)]
268 >>> def key(pair): return pair[0]
269 >>> list(_iter_matching(seq1, seq2, key))
270 [(1, (1, 2), (1, 3)), (2, (2, 3), None), (3, None, (3, 4))]
271 """
272 seq1.sort(key=key)
273 seq2.sort(key=key)
274 sentinel = object()
275 def next(it):
276 try:
277 o = it.next()
278 return (key(o), o)
279 except StopIteration:
280 return (sentinel, None)
281 iter1 = iter(seq1)
282 iter2 = iter(seq2)
283 k1, o1 = next(iter1)
284 k2, o2 = next(iter2)
285 while k1 is not sentinel or k2 is not sentinel:
286 if k1 is sentinel:
287 yield (k2, None, o2)
288 k2, o2 = next(iter2)
289 elif k2 is sentinel:
290 yield (k1, o1, None)
291 k1, o1 = next(iter1)
292 elif k1 == k2:
293 yield (k1, o1, o2)
294 k1, o1 = next(iter1)
295 k2, o2 = next(iter2)
296 elif k1 < k2:
297 yield (k1, o1, None)
298 k1, o1 = next(iter1)
299 else: # so k1 > k2...
300 yield (k2, None, o2)
301 k2, o2 = next(iter2)
302
303
304def _test_run_difference(test_run1, test_run2, cases=None):
305 test_results1 = list(test_run1.test_results.all().select_related('test_case'))
306 test_results2 = list(test_run2.test_results.all().select_related('test_case'))
307 def key(tr):
308 return tr.test_case.test_case_id
309 differences = []
310 for tc_id, tc1, tc2 in _iter_matching(test_results1, test_results2, key):
311 if cases is not None and tc_id not in cases:
312 continue
313 if tc1:
314 tc1 = tc1.result_code
315 if tc2:
316 tc2 = tc2.result_code
317 if tc1 != tc2:
318 differences.append({
319 'test_case_id': tc_id,
320 'first_result': tc1,
321 'second_result': tc2,
322 })
323 return differences
324
325
326@BreadCrumb(
327 "Comparing builds {tag1} and {tag2}",
328 parent=filter_detail,
329 needs=['username', 'name', 'tag1', 'tag2'])
330def compare_matches(request, username, name, tag1, tag2):
331 filter = TestRunFilter.objects.get(owner__username=username, name=name)
332 if not filter.public and filter.owner != request.user:
333 raise PermissionDenied()
334 filter_data = filter.as_data()
335 matches = evaluate_filter(request.user, filter_data)
336 match1, match2 = matches.with_tags(tag1, tag2)
337 test_cases_for_test_id = {}
338 for test in filter_data['tests']:
339 test_cases = test['test_cases']
340 if test_cases:
341 test_cases = set([tc.test_case_id for tc in test_cases])
342 else:
343 test_cases = None
344 test_cases_for_test_id[test['test'].test_id] = test_cases
345 test_run_info = []
346 def key(tr):
347 return tr.test.test_id
348 for key, tr1, tr2 in _iter_matching(match1.test_runs, match2.test_runs, key):
349 if tr1 is None:
350 table = None
351 only = 'right'
352 tr = tr2
353 tag = tag2
354 cases = None
355 elif tr2 is None:
356 table = None
357 only = 'left'
358 tr = tr1
359 tag = tag1
360 cases = None
361 else:
362 only = None
363 tr = None
364 tag = None
365 cases = test_cases_for_test_id.get(key)
366 test_result_differences = _test_run_difference(tr1, tr2, cases)
367 if test_result_differences:
368 table = TestResultDifferenceTable(
369 "test-result-difference-" + escape(key), data=test_result_differences)
370 table.base_columns['first_result'].verbose_name = mark_safe(
371 '<a href="%s">build %s: %s</a>' % (
372 tr1.get_absolute_url(), escape(tag1), escape(key)))
373 table.base_columns['second_result'].verbose_name = mark_safe(
374 '<a href="%s">build %s: %s</a>' % (
375 tr2.get_absolute_url(), escape(tag2), escape(key)))
376 else:
377 table = None
378 if cases:
379 cases = sorted(cases)
380 if len(cases) > 1:
381 cases = ', '.join(cases[:-1]) + ' or ' + cases[-1]
382 else:
383 cases = cases[0]
384 test_run_info.append(dict(
385 only=only,
386 key=key,
387 table=table,
388 tr=tr,
389 tag=tag,
390 cases=cases))
391 return render_to_response(
392 "dashboard_app/filter_compare_matches.html", {
393 'test_run_info': test_run_info,
394 'bread_crumb_trail': BreadCrumbTrail.leading_to(
395 compare_matches,
396 name=name,
397 username=username,
398 tag1=tag1,
399 tag2=tag2),
400 }, RequestContext(request))

Subscribers

People subscribed via source and target branches

to all changes: