Merge lp:~mwhudson/lava-dashboard/compare-testrun-view into lp:lava-dashboard
- compare-testrun-view
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan (community) | Approve | ||
Review via email: mp+142421@code.launchpad.net |
Commit message
Description of the change
This branch adds a way to compare matches of a filter. You can play with it at http://
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 : | # |
- 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
1 | === modified file 'dashboard_app/filters.py' | |||
2 | --- dashboard_app/filters.py 2012-12-18 00:47:08 +0000 | |||
3 | +++ dashboard_app/filters.py 2013-01-10 20:39:21 +0000 | |||
4 | @@ -300,6 +300,22 @@ | |||
5 | 300 | q = self.queryset.filter(bundle__uploaded_on__gt=since) | 300 | q = self.queryset.filter(bundle__uploaded_on__gt=since) |
6 | 301 | return self._wrap(q) | 301 | return self._wrap(q) |
7 | 302 | 302 | ||
8 | 303 | def with_tags(self, tag1, tag2): | ||
9 | 304 | if self.key == 'build_number': | ||
10 | 305 | q = self.queryset.extra( | ||
11 | 306 | where=['convert_to_integer("dashboard_app_namedattribute"."value") in (%s, %s)' % (tag1, tag2)] | ||
12 | 307 | ) | ||
13 | 308 | else: | ||
14 | 309 | tag1 = datetime.datetime.strptime(tag1, "%Y-%m-%d %H:%M:%S.%f") | ||
15 | 310 | tag2 = datetime.datetime.strptime(tag2, "%Y-%m-%d %H:%M:%S.%f") | ||
16 | 311 | q = self.queryset.filter(bundle__uploaded_on__in=(tag1, tag2)) | ||
17 | 312 | matches = list(self._wrap(q)) | ||
18 | 313 | if matches[0].tag == tag1: | ||
19 | 314 | return matches | ||
20 | 315 | else: | ||
21 | 316 | matches.reverse() | ||
22 | 317 | return matches | ||
23 | 318 | |||
24 | 303 | def count(self): | 319 | def count(self): |
25 | 304 | return self.queryset.count() | 320 | return self.queryset.count() |
26 | 305 | 321 | ||
27 | 306 | 322 | ||
28 | === added file 'dashboard_app/static/css/filter-detail.css' | |||
29 | --- dashboard_app/static/css/filter-detail.css 1970-01-01 00:00:00 +0000 | |||
30 | +++ dashboard_app/static/css/filter-detail.css 2013-01-10 20:39:21 +0000 | |||
31 | @@ -0,0 +1,52 @@ | |||
32 | 1 | table.select-compare1 td { cursor: pointer; } | ||
33 | 2 | table.select-compare1 tr.even td { | ||
34 | 3 | background-color: #ccf; | ||
35 | 4 | } | ||
36 | 5 | table.select-compare1 tr.even.hover td { | ||
37 | 6 | background-color: #77f; | ||
38 | 7 | } | ||
39 | 8 | table.select-compare1 tr.odd td { | ||
40 | 9 | background-color: #aaf; | ||
41 | 10 | } | ||
42 | 11 | table.select-compare1 tr.odd.hover td { | ||
43 | 12 | background-color: #77f; | ||
44 | 13 | } | ||
45 | 14 | |||
46 | 15 | table.select-compare2 td { cursor: pointer; } | ||
47 | 16 | table.select-compare2 tr.even td { | ||
48 | 17 | background-color: #fcc; | ||
49 | 18 | } | ||
50 | 19 | table.select-compare2 tr.odd td { | ||
51 | 20 | background-color: #faa; | ||
52 | 21 | } | ||
53 | 22 | table.select-compare2 tr.selected-1 td { | ||
54 | 23 | background-color: #77f; | ||
55 | 24 | } | ||
56 | 25 | table.select-compare2 tr.selected-1.hover td { | ||
57 | 26 | background-color: #77f; | ||
58 | 27 | } | ||
59 | 28 | table.select-compare2 tr.hover td { | ||
60 | 29 | background-color: #f77; | ||
61 | 30 | } | ||
62 | 31 | table.select-compare3 tr.selected-1 td { | ||
63 | 32 | background-color: #77f; | ||
64 | 33 | } | ||
65 | 34 | table.select-compare3 tr.selected-1.hover td { | ||
66 | 35 | background-color: #77f; | ||
67 | 36 | } | ||
68 | 37 | table.select-compare3 tr.selected-2 td { | ||
69 | 38 | background-color: #f77; | ||
70 | 39 | } | ||
71 | 40 | table.select-compare3 tr.selected-2.hover td { | ||
72 | 41 | background-color: #f77; | ||
73 | 42 | } | ||
74 | 43 | table.select-compare3 tr.selected-1 { | ||
75 | 44 | cursor: pointer; | ||
76 | 45 | } | ||
77 | 46 | table.select-compare3 tr.selected-2 { | ||
78 | 47 | cursor: pointer; | ||
79 | 48 | } | ||
80 | 49 | #filter-table input { | ||
81 | 50 | margin-top: 0; | ||
82 | 51 | margin-bottom: 0; | ||
83 | 52 | } | ||
84 | 0 | \ No newline at end of file | 53 | \ No newline at end of file |
85 | 1 | 54 | ||
86 | === added file 'dashboard_app/static/js/filter-detail.js' | |||
87 | --- dashboard_app/static/js/filter-detail.js 1970-01-01 00:00:00 +0000 | |||
88 | +++ dashboard_app/static/js/filter-detail.js 2013-01-10 20:39:21 +0000 | |||
89 | @@ -0,0 +1,135 @@ | |||
90 | 1 | var compareState = 0; | ||
91 | 2 | var compare1 = null, compare2 = null; | ||
92 | 3 | function cancelCompare () { | ||
93 | 4 | $("#filter-table").removeClass("select-compare1"); | ||
94 | 5 | $("#filter-table").removeClass("select-compare2"); | ||
95 | 6 | $("#filter-table").removeClass("select-compare3"); | ||
96 | 7 | $("#filter-table tr").removeClass("selected-1"); | ||
97 | 8 | $("#filter-table tr").removeClass("selected-2"); | ||
98 | 9 | $("#filter-table tr").unbind("click"); | ||
99 | 10 | $("#filter-table tr").unbind("hover"); | ||
100 | 11 | $("#filter-table tr").each(removeCheckbox); | ||
101 | 12 | $("#first-prompt").hide(); | ||
102 | 13 | $("#second-prompt").hide(); | ||
103 | 14 | $("#third-prompt").hide(); | ||
104 | 15 | $("#compare-button").button({label:"Compare builds"}); | ||
105 | 16 | compareState = 0; | ||
106 | 17 | } | ||
107 | 18 | function startCompare () { | ||
108 | 19 | $("#compare-button").button({label:"Cancel"}); | ||
109 | 20 | $("#filter-table").addClass("select-compare1"); | ||
110 | 21 | $("#filter-table tr").click(rowClickHandler); | ||
111 | 22 | $("#filter-table tr").each(insertCheckbox); | ||
112 | 23 | $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut); | ||
113 | 24 | $("#first-prompt").show(); | ||
114 | 25 | compareState = 1; | ||
115 | 26 | } | ||
116 | 27 | function tagFromRow(tr) { | ||
117 | 28 | var firstCell = $(tr).find("td:eq(0)"); | ||
118 | 29 | return { | ||
119 | 30 | machinetag: firstCell.find("span").data("machinetag"), | ||
120 | 31 | usertag: firstCell.text() | ||
121 | 32 | }; | ||
122 | 33 | } | ||
123 | 34 | function rowClickHandler() { | ||
124 | 35 | if (compareState == 1) { | ||
125 | 36 | compare1 = tagFromRow($(this)); | ||
126 | 37 | $(this).addClass("selected-1"); | ||
127 | 38 | $(this).find("input").attr("checked", true); | ||
128 | 39 | $("#p2-build").text(compare1.usertag); | ||
129 | 40 | $("#first-prompt").hide(); | ||
130 | 41 | $("#second-prompt").show(); | ||
131 | 42 | $("#filter-table").removeClass("select-compare1"); | ||
132 | 43 | $("#filter-table").addClass("select-compare2"); | ||
133 | 44 | compareState = 2; | ||
134 | 45 | } else if (compareState == 2) { | ||
135 | 46 | var thistag = tagFromRow($(this)); | ||
136 | 47 | if (compare1.machinetag == thistag.machinetag) { | ||
137 | 48 | cancelCompare(); | ||
138 | 49 | startCompare(); | ||
139 | 50 | } else { | ||
140 | 51 | compare2 = thistag; | ||
141 | 52 | $(this).find("input").attr("checked", true); | ||
142 | 53 | $(this).addClass("selected-2"); | ||
143 | 54 | $("#second-prompt").hide(); | ||
144 | 55 | $("#third-prompt").show(); | ||
145 | 56 | $("#filter-table").removeClass("select-compare2"); | ||
146 | 57 | $("#filter-table").addClass("select-compare3"); | ||
147 | 58 | $("#filter-table input").attr("disabled", true); | ||
148 | 59 | $("#filter-table .selected-1 input").attr("disabled", false); | ||
149 | 60 | $("#filter-table .selected-2 input").attr("disabled", false); | ||
150 | 61 | $("#p3-build-1").text(compare1.usertag); | ||
151 | 62 | $("#p3-build-2").text(compare2.usertag); | ||
152 | 63 | $("#third-prompt a").attr("href", window.location + '/+compare/' + compare1.machinetag + '/' + compare2.machinetag); | ||
153 | 64 | compareState = 3; | ||
154 | 65 | } | ||
155 | 66 | } else if (compareState == 3) { | ||
156 | 67 | var thistag = tagFromRow($(this)); | ||
157 | 68 | if (thistag.machinetag == compare1.machinetag || thistag.machinetag == compare2.machinetag) { | ||
158 | 69 | $("#second-prompt").show(); | ||
159 | 70 | $("#third-prompt").hide(); | ||
160 | 71 | $("#filter-table").addClass("select-compare2"); | ||
161 | 72 | $("#filter-table").removeClass("select-compare3"); | ||
162 | 73 | $("#filter-table input").attr("disabled", false); | ||
163 | 74 | compareState = 2; | ||
164 | 75 | $(this).find("input").attr("checked", false); | ||
165 | 76 | if (thistag.machinetag == compare1.machinetag) { | ||
166 | 77 | compare1 = compare2; | ||
167 | 78 | $("#filter-table .selected-1").removeClass("selected-1"); | ||
168 | 79 | $("#filter-table .selected-2").addClass("selected-1"); | ||
169 | 80 | $("#p2-build").text(compare1.usertag); | ||
170 | 81 | } | ||
171 | 82 | $("#filter-table .selected-2").removeClass("selected-2"); | ||
172 | 83 | } | ||
173 | 84 | } | ||
174 | 85 | tagFromRow(this); | ||
175 | 86 | } | ||
176 | 87 | function rowHoverHandlerIn() { | ||
177 | 88 | $(this).addClass("hover"); | ||
178 | 89 | } | ||
179 | 90 | function rowHoverHandlerOut() { | ||
180 | 91 | $(this).removeClass("hover"); | ||
181 | 92 | } | ||
182 | 93 | function insertCheckbox() { | ||
183 | 94 | var row = $(this); | ||
184 | 95 | var checkbox = $('<input type="checkbox">'); | ||
185 | 96 | row.find("td:first").prepend(checkbox); | ||
186 | 97 | } | ||
187 | 98 | function removeCheckbox() { | ||
188 | 99 | var row = $(this); | ||
189 | 100 | row.find('input').remove(); | ||
190 | 101 | } | ||
191 | 102 | $(window).load( | ||
192 | 103 | function () { | ||
193 | 104 | $("#filter-table").dataTable().fnSettings().fnRowCallback = function(tr, data, index) { | ||
194 | 105 | if (compareState) { | ||
195 | 106 | insertCheckbox.call(tr); | ||
196 | 107 | $(tr).click(rowClickHandler); | ||
197 | 108 | $("#filter-table tr").hover(rowHoverHandlerIn, rowHoverHandlerOut); | ||
198 | 109 | if (compareState >= 2 && tagFromRow(tr).machinetag == compare1.machinetag) { | ||
199 | 110 | $(tr).addClass("selected-1"); | ||
200 | 111 | $(tr).find("input").attr("checked", true); | ||
201 | 112 | } | ||
202 | 113 | if (compareState >= 3) { | ||
203 | 114 | if (tagFromRow(tr).machinetag == compare2.machinetag) { | ||
204 | 115 | $(tr).addClass("selected-2"); | ||
205 | 116 | $(tr).find("input").attr("checked", true); | ||
206 | 117 | } else if (tagFromRow(tr).machinetag != compare1.machinetag) { | ||
207 | 118 | $(tr).find("input").attr("disabled", true); | ||
208 | 119 | } | ||
209 | 120 | } | ||
210 | 121 | } | ||
211 | 122 | return tr; | ||
212 | 123 | }; | ||
213 | 124 | $("#compare-button").button(); | ||
214 | 125 | $("#compare-button").click( | ||
215 | 126 | function (e) { | ||
216 | 127 | if (compareState == 0) { | ||
217 | 128 | startCompare(); | ||
218 | 129 | } else { | ||
219 | 130 | cancelCompare(); | ||
220 | 131 | } | ||
221 | 132 | } | ||
222 | 133 | ); | ||
223 | 134 | } | ||
224 | 135 | ); | ||
225 | 0 | 136 | ||
226 | === added file 'dashboard_app/templates/dashboard_app/filter_compare_matches.html' | |||
227 | --- dashboard_app/templates/dashboard_app/filter_compare_matches.html 1970-01-01 00:00:00 +0000 | |||
228 | +++ dashboard_app/templates/dashboard_app/filter_compare_matches.html 2013-01-10 20:39:21 +0000 | |||
229 | @@ -0,0 +1,28 @@ | |||
230 | 1 | {% extends "dashboard_app/_content.html" %} | ||
231 | 2 | |||
232 | 3 | {% load django_tables2 %} | ||
233 | 4 | |||
234 | 5 | {% block extrahead %} | ||
235 | 6 | {{ block.super }} | ||
236 | 7 | <style type="text/css"> | ||
237 | 8 | th.orderable.sortable a { | ||
238 | 9 | color: rgb(0, 136, 204); | ||
239 | 10 | text-decoration: underline; | ||
240 | 11 | } | ||
241 | 12 | </style> | ||
242 | 13 | {% endblock %} | ||
243 | 14 | |||
244 | 15 | {% block content %} | ||
245 | 16 | {% for trinfo in test_run_info %} | ||
246 | 17 | <h3>{{ trinfo.key }} results</h3> | ||
247 | 18 | {% if trinfo.only %} | ||
248 | 19 | <p style="text-align: {{ trinfo.only }}"> | ||
249 | 20 | Results were only present in <a href="{{ trinfo.tr.get_absolute_url }}">build {{ trinfo.tag }}</a>. | ||
250 | 21 | </p> | ||
251 | 22 | {% elif trinfo.table %} | ||
252 | 23 | {% render_table trinfo.table %} | ||
253 | 24 | {% else %} | ||
254 | 25 | <p>No difference in {{ trinfo.key }} results{% if trinfo.cases %} for {{ trinfo.cases }}{% endif %}.</p> | ||
255 | 26 | {% endif %} | ||
256 | 27 | {% endfor %} | ||
257 | 28 | {% endblock %} | ||
258 | 0 | 29 | ||
259 | === modified file 'dashboard_app/templates/dashboard_app/filter_detail.html' | |||
260 | --- dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-09 00:05:14 +0000 | |||
261 | +++ dashboard_app/templates/dashboard_app/filter_detail.html 2013-01-10 20:39:21 +0000 | |||
262 | @@ -2,6 +2,12 @@ | |||
263 | 2 | {% load i18n %} | 2 | {% load i18n %} |
264 | 3 | {% load django_tables2 %} | 3 | {% load django_tables2 %} |
265 | 4 | 4 | ||
266 | 5 | {% block extrahead %} | ||
267 | 6 | {{ block.super }} | ||
268 | 7 | <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-detail.css"/> | ||
269 | 8 | <script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-detail.js"></script> | ||
270 | 9 | {% endblock %} | ||
271 | 10 | |||
272 | 5 | {% block content %} | 11 | {% block content %} |
273 | 6 | 12 | ||
274 | 7 | <h1>Filter {{ filter.name }}</h1> | 13 | <h1>Filter {{ filter.name }}</h1> |
275 | @@ -27,4 +33,17 @@ | |||
276 | 27 | 33 | ||
277 | 28 | {% render_table filter_table %} | 34 | {% render_table filter_table %} |
278 | 29 | 35 | ||
279 | 36 | <p> | ||
280 | 37 | <button id="compare-button">Compare builds</button> | ||
281 | 38 | <span id="first-prompt" style="display:none"> | ||
282 | 39 | Click a build to compare. | ||
283 | 40 | </span> | ||
284 | 41 | <span id="second-prompt" style="display:none"> | ||
285 | 42 | Click build to compare with build <span id="p2-build">XXX</span>. | ||
286 | 43 | </span> | ||
287 | 44 | <span id="third-prompt" style="display:none"> | ||
288 | 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>. | ||
289 | 46 | </span> | ||
290 | 47 | </p> | ||
291 | 48 | |||
292 | 30 | {% endblock %} | 49 | {% endblock %} |
293 | 31 | 50 | ||
294 | === modified file 'dashboard_app/urls.py' | |||
295 | --- dashboard_app/urls.py 2012-12-11 02:01:37 +0000 | |||
296 | +++ dashboard_app/urls.py 2013-01-10 20:39:21 +0000 | |||
297 | @@ -45,7 +45,8 @@ | |||
298 | 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'), |
299 | 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'), |
300 | 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'), |
302 | 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'), |
303 | 49 | url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, | ||
304 | 49 | name='dashboard_app.views.dashboard_xml_rpc_handler', | 50 | name='dashboard_app.views.dashboard_xml_rpc_handler', |
305 | 50 | kwargs={ | 51 | kwargs={ |
306 | 51 | 'mapper': legacy_mapper, | 52 | 'mapper': legacy_mapper, |
307 | 52 | 53 | ||
308 | === modified file 'dashboard_app/views/__init__.py' | |||
309 | --- dashboard_app/views/__init__.py 2012-12-17 00:10:53 +0000 | |||
310 | +++ dashboard_app/views/__init__.py 2013-01-10 20:39:21 +0000 | |||
311 | @@ -38,7 +38,6 @@ | |||
312 | 38 | ) | 38 | ) |
313 | 39 | from django.shortcuts import render_to_response, redirect, get_object_or_404 | 39 | from django.shortcuts import render_to_response, redirect, get_object_or_404 |
314 | 40 | from django.template import RequestContext, loader | 40 | from django.template import RequestContext, loader |
315 | 41 | from django.utils.html import escape | ||
316 | 42 | from django.utils.safestring import mark_safe | 41 | from django.utils.safestring import mark_safe |
317 | 43 | from django.views.generic.list_detail import object_list, object_detail | 42 | from django.views.generic.list_detail import object_list, object_detail |
318 | 44 | 43 | ||
319 | @@ -825,3 +824,4 @@ | |||
320 | 825 | pk=effort.pk) | 824 | pk=effort.pk) |
321 | 826 | }) | 825 | }) |
322 | 827 | return HttpResponse(t.render(c)) | 826 | return HttpResponse(t.render(c)) |
323 | 827 | |||
324 | 828 | 828 | ||
325 | === modified file 'dashboard_app/views/filters/tables.py' | |||
326 | --- dashboard_app/views/filters/tables.py 2012-12-13 00:51:07 +0000 | |||
327 | +++ dashboard_app/views/filters/tables.py 2013-01-10 20:39:21 +0000 | |||
328 | @@ -16,8 +16,11 @@ | |||
329 | 16 | # You should have received a copy of the GNU Affero General Public License | 16 | # You should have received a copy of the GNU Affero General Public License |
330 | 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/>. |
331 | 18 | 18 | ||
332 | 19 | import datetime | ||
333 | 19 | import operator | 20 | import operator |
334 | 20 | 21 | ||
335 | 22 | from django.conf import settings | ||
336 | 23 | from django.template import defaultfilters | ||
337 | 21 | from django.utils.html import escape | 24 | from django.utils.html import escape |
338 | 22 | from django.utils.safestring import mark_safe | 25 | from django.utils.safestring import mark_safe |
339 | 23 | 26 | ||
340 | @@ -184,6 +187,12 @@ | |||
341 | 184 | self.base_columns.insert(0, 'bundle_stream', bundle_stream_col) | 187 | self.base_columns.insert(0, 'bundle_stream', bundle_stream_col) |
342 | 185 | self.base_columns.insert(0, 'tag', tag_col) | 188 | self.base_columns.insert(0, 'tag', tag_col) |
343 | 186 | 189 | ||
344 | 190 | def render_tag(self, value): | ||
345 | 191 | if isinstance(value, datetime.datetime): | ||
346 | 192 | strvalue = defaultfilters.date(value, settings.DATETIME_FORMAT) | ||
347 | 193 | else: | ||
348 | 194 | strvalue = value | ||
349 | 195 | return mark_safe('<span data-machinetag="%s">%s</span>' % (escape(str(value)), strvalue)) | ||
350 | 187 | tag = Column() | 196 | tag = Column() |
351 | 188 | 197 | ||
352 | 189 | def render_bundle_stream(self, record): | 198 | def render_bundle_stream(self, record): |
353 | @@ -225,3 +234,29 @@ | |||
354 | 225 | datatable_opts.update({ | 234 | datatable_opts.update({ |
355 | 226 | "iDisplayLength": 10, | 235 | "iDisplayLength": 10, |
356 | 227 | }) | 236 | }) |
357 | 237 | |||
358 | 238 | |||
359 | 239 | class TestResultDifferenceTable(DataTablesTable): | ||
360 | 240 | test_case_id = Column(verbose_name=mark_safe('test_case_id')) | ||
361 | 241 | first_result = TemplateColumn(''' | ||
362 | 242 | {% if record.first_result %} | ||
363 | 243 | <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.first_result }}.png" | ||
364 | 244 | alt="{{ record.first_result }}" width="16" height="16" border="0"/>{{ record.first_result }} | ||
365 | 245 | {% else %} | ||
366 | 246 | <i>missing</i> | ||
367 | 247 | {% endif %} | ||
368 | 248 | ''') | ||
369 | 249 | second_result = TemplateColumn(''' | ||
370 | 250 | {% if record.second_result %} | ||
371 | 251 | <img src="{{ STATIC_URL }}dashboard_app/images/icon-{{ record.second_result }}.png" | ||
372 | 252 | alt="{{ record.second_result }}" width="16" height="16" border="0"/>{{ record.second_result }} | ||
373 | 253 | {% else %} | ||
374 | 254 | <i>missing</i> | ||
375 | 255 | {% endif %} | ||
376 | 256 | ''') | ||
377 | 257 | |||
378 | 258 | datatable_opts = { | ||
379 | 259 | 'iDisplayLength': 25, | ||
380 | 260 | 'sPaginationType': "full_numbers", | ||
381 | 261 | } | ||
382 | 262 | |||
383 | 228 | 263 | ||
384 | === modified file 'dashboard_app/views/filters/views.py' | |||
385 | --- dashboard_app/views/filters/views.py 2012-11-27 05:07:00 +0000 | |||
386 | +++ dashboard_app/views/filters/views.py 2013-01-10 20:39:21 +0000 | |||
387 | @@ -25,12 +25,17 @@ | |||
388 | 25 | from django.http import HttpResponse, HttpResponseRedirect | 25 | from django.http import HttpResponse, HttpResponseRedirect |
389 | 26 | from django.shortcuts import render_to_response | 26 | from django.shortcuts import render_to_response |
390 | 27 | from django.template import RequestContext | 27 | from django.template import RequestContext |
391 | 28 | from django.utils.html import escape | ||
392 | 29 | from django.utils.safestring import mark_safe | ||
393 | 28 | 30 | ||
394 | 29 | from lava_server.bread_crumbs import ( | 31 | from lava_server.bread_crumbs import ( |
395 | 30 | BreadCrumb, | 32 | BreadCrumb, |
396 | 31 | BreadCrumbTrail, | 33 | BreadCrumbTrail, |
397 | 32 | ) | 34 | ) |
398 | 33 | 35 | ||
399 | 36 | from dashboard_app.filters import ( | ||
400 | 37 | evaluate_filter, | ||
401 | 38 | ) | ||
402 | 34 | from dashboard_app.models import ( | 39 | from dashboard_app.models import ( |
403 | 35 | NamedAttribute, | 40 | NamedAttribute, |
404 | 36 | Test, | 41 | Test, |
405 | @@ -39,7 +44,9 @@ | |||
406 | 39 | TestRunFilter, | 44 | TestRunFilter, |
407 | 40 | TestRunFilterSubscription, | 45 | TestRunFilterSubscription, |
408 | 41 | ) | 46 | ) |
410 | 42 | from dashboard_app.views import index | 47 | from dashboard_app.views import ( |
411 | 48 | index, | ||
412 | 49 | ) | ||
413 | 43 | from dashboard_app.views.filters.forms import ( | 50 | from dashboard_app.views.filters.forms import ( |
414 | 44 | TestRunFilterForm, | 51 | TestRunFilterForm, |
415 | 45 | TestRunFilterSubscriptionForm, | 52 | TestRunFilterSubscriptionForm, |
416 | @@ -48,6 +55,7 @@ | |||
417 | 48 | FilterTable, | 55 | FilterTable, |
418 | 49 | FilterPreviewTable, | 56 | FilterPreviewTable, |
419 | 50 | PublicFiltersTable, | 57 | PublicFiltersTable, |
420 | 58 | TestResultDifferenceTable, | ||
421 | 51 | UserFiltersTable, | 59 | UserFiltersTable, |
422 | 52 | ) | 60 | ) |
423 | 53 | 61 | ||
424 | @@ -248,3 +256,145 @@ | |||
425 | 248 | json.dumps(list(result)), | 256 | json.dumps(list(result)), |
426 | 249 | mimetype='application/json') | 257 | mimetype='application/json') |
427 | 250 | 258 | ||
428 | 259 | |||
429 | 260 | def _iter_matching(seq1, seq2, key): | ||
430 | 261 | """Iterate over sequences in the order given by the key function, matching | ||
431 | 262 | elements with matching key values. | ||
432 | 263 | |||
433 | 264 | For example: | ||
434 | 265 | |||
435 | 266 | >>> seq1 = [(1, 2), (2, 3)] | ||
436 | 267 | >>> seq2 = [(1, 3), (3, 4)] | ||
437 | 268 | >>> def key(pair): return pair[0] | ||
438 | 269 | >>> list(_iter_matching(seq1, seq2, key)) | ||
439 | 270 | [(1, (1, 2), (1, 3)), (2, (2, 3), None), (3, None, (3, 4))] | ||
440 | 271 | """ | ||
441 | 272 | seq1.sort(key=key) | ||
442 | 273 | seq2.sort(key=key) | ||
443 | 274 | sentinel = object() | ||
444 | 275 | def next(it): | ||
445 | 276 | try: | ||
446 | 277 | o = it.next() | ||
447 | 278 | return (key(o), o) | ||
448 | 279 | except StopIteration: | ||
449 | 280 | return (sentinel, None) | ||
450 | 281 | iter1 = iter(seq1) | ||
451 | 282 | iter2 = iter(seq2) | ||
452 | 283 | k1, o1 = next(iter1) | ||
453 | 284 | k2, o2 = next(iter2) | ||
454 | 285 | while k1 is not sentinel or k2 is not sentinel: | ||
455 | 286 | if k1 is sentinel: | ||
456 | 287 | yield (k2, None, o2) | ||
457 | 288 | k2, o2 = next(iter2) | ||
458 | 289 | elif k2 is sentinel: | ||
459 | 290 | yield (k1, o1, None) | ||
460 | 291 | k1, o1 = next(iter1) | ||
461 | 292 | elif k1 == k2: | ||
462 | 293 | yield (k1, o1, o2) | ||
463 | 294 | k1, o1 = next(iter1) | ||
464 | 295 | k2, o2 = next(iter2) | ||
465 | 296 | elif k1 < k2: | ||
466 | 297 | yield (k1, o1, None) | ||
467 | 298 | k1, o1 = next(iter1) | ||
468 | 299 | else: # so k1 > k2... | ||
469 | 300 | yield (k2, None, o2) | ||
470 | 301 | k2, o2 = next(iter2) | ||
471 | 302 | |||
472 | 303 | |||
473 | 304 | def _test_run_difference(test_run1, test_run2, cases=None): | ||
474 | 305 | test_results1 = list(test_run1.test_results.all().select_related('test_case')) | ||
475 | 306 | test_results2 = list(test_run2.test_results.all().select_related('test_case')) | ||
476 | 307 | def key(tr): | ||
477 | 308 | return tr.test_case.test_case_id | ||
478 | 309 | differences = [] | ||
479 | 310 | for tc_id, tc1, tc2 in _iter_matching(test_results1, test_results2, key): | ||
480 | 311 | if cases is not None and tc_id not in cases: | ||
481 | 312 | continue | ||
482 | 313 | if tc1: | ||
483 | 314 | tc1 = tc1.result_code | ||
484 | 315 | if tc2: | ||
485 | 316 | tc2 = tc2.result_code | ||
486 | 317 | if tc1 != tc2: | ||
487 | 318 | differences.append({ | ||
488 | 319 | 'test_case_id': tc_id, | ||
489 | 320 | 'first_result': tc1, | ||
490 | 321 | 'second_result': tc2, | ||
491 | 322 | }) | ||
492 | 323 | return differences | ||
493 | 324 | |||
494 | 325 | |||
495 | 326 | @BreadCrumb( | ||
496 | 327 | "Comparing builds {tag1} and {tag2}", | ||
497 | 328 | parent=filter_detail, | ||
498 | 329 | needs=['username', 'name', 'tag1', 'tag2']) | ||
499 | 330 | def compare_matches(request, username, name, tag1, tag2): | ||
500 | 331 | filter = TestRunFilter.objects.get(owner__username=username, name=name) | ||
501 | 332 | if not filter.public and filter.owner != request.user: | ||
502 | 333 | raise PermissionDenied() | ||
503 | 334 | filter_data = filter.as_data() | ||
504 | 335 | matches = evaluate_filter(request.user, filter_data) | ||
505 | 336 | match1, match2 = matches.with_tags(tag1, tag2) | ||
506 | 337 | test_cases_for_test_id = {} | ||
507 | 338 | for test in filter_data['tests']: | ||
508 | 339 | test_cases = test['test_cases'] | ||
509 | 340 | if test_cases: | ||
510 | 341 | test_cases = set([tc.test_case_id for tc in test_cases]) | ||
511 | 342 | else: | ||
512 | 343 | test_cases = None | ||
513 | 344 | test_cases_for_test_id[test['test'].test_id] = test_cases | ||
514 | 345 | test_run_info = [] | ||
515 | 346 | def key(tr): | ||
516 | 347 | return tr.test.test_id | ||
517 | 348 | for key, tr1, tr2 in _iter_matching(match1.test_runs, match2.test_runs, key): | ||
518 | 349 | if tr1 is None: | ||
519 | 350 | table = None | ||
520 | 351 | only = 'right' | ||
521 | 352 | tr = tr2 | ||
522 | 353 | tag = tag2 | ||
523 | 354 | cases = None | ||
524 | 355 | elif tr2 is None: | ||
525 | 356 | table = None | ||
526 | 357 | only = 'left' | ||
527 | 358 | tr = tr1 | ||
528 | 359 | tag = tag1 | ||
529 | 360 | cases = None | ||
530 | 361 | else: | ||
531 | 362 | only = None | ||
532 | 363 | tr = None | ||
533 | 364 | tag = None | ||
534 | 365 | cases = test_cases_for_test_id.get(key) | ||
535 | 366 | test_result_differences = _test_run_difference(tr1, tr2, cases) | ||
536 | 367 | if test_result_differences: | ||
537 | 368 | table = TestResultDifferenceTable( | ||
538 | 369 | "test-result-difference-" + escape(key), data=test_result_differences) | ||
539 | 370 | table.base_columns['first_result'].verbose_name = mark_safe( | ||
540 | 371 | '<a href="%s">build %s: %s</a>' % ( | ||
541 | 372 | tr1.get_absolute_url(), escape(tag1), escape(key))) | ||
542 | 373 | table.base_columns['second_result'].verbose_name = mark_safe( | ||
543 | 374 | '<a href="%s">build %s: %s</a>' % ( | ||
544 | 375 | tr2.get_absolute_url(), escape(tag2), escape(key))) | ||
545 | 376 | else: | ||
546 | 377 | table = None | ||
547 | 378 | if cases: | ||
548 | 379 | cases = sorted(cases) | ||
549 | 380 | if len(cases) > 1: | ||
550 | 381 | cases = ', '.join(cases[:-1]) + ' or ' + cases[-1] | ||
551 | 382 | else: | ||
552 | 383 | cases = cases[0] | ||
553 | 384 | test_run_info.append(dict( | ||
554 | 385 | only=only, | ||
555 | 386 | key=key, | ||
556 | 387 | table=table, | ||
557 | 388 | tr=tr, | ||
558 | 389 | tag=tag, | ||
559 | 390 | cases=cases)) | ||
560 | 391 | return render_to_response( | ||
561 | 392 | "dashboard_app/filter_compare_matches.html", { | ||
562 | 393 | 'test_run_info': test_run_info, | ||
563 | 394 | 'bread_crumb_trail': BreadCrumbTrail.leading_to( | ||
564 | 395 | compare_matches, | ||
565 | 396 | name=name, | ||
566 | 397 | username=username, | ||
567 | 398 | tag1=tag1, | ||
568 | 399 | tag2=tag2), | ||
569 | 400 | }, RequestContext(request)) |
I've tried to make the way of getting to the comparison page a little less odd. Let me know what you think.