Merge lp:~linaro-validation/lava-dashboard/0.6-wip into lp:lava-dashboard
- 0.6-wip
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 248 |
Proposed branch: | lp:~linaro-validation/lava-dashboard/0.6-wip |
Merge into: | lp:lava-dashboard |
Diff against target: |
2319 lines (+1013/-739) 31 files modified
dashboard_app/admin.py (+7/-2) dashboard_app/dataview.py (+0/-284) dashboard_app/dispatcher.py (+0/-188) dashboard_app/models.py (+218/-16) dashboard_app/repositories/__init__.py (+11/-9) dashboard_app/repositories/common.py (+47/-0) dashboard_app/repositories/data_report.py (+7/-26) dashboard_app/repositories/data_view.py (+133/-0) dashboard_app/static/js/jquery.dashboard.js (+1/-1) dashboard_app/templates/dashboard_app/_extension_navigation.html (+2/-0) dashboard_app/templates/dashboard_app/bundle_detail.html (+14/-0) dashboard_app/templates/dashboard_app/data_view_list.html (+1/-1) dashboard_app/templates/dashboard_app/image_status_detail.html (+71/-0) dashboard_app/templates/dashboard_app/image_status_list.html (+38/-0) dashboard_app/templates/dashboard_app/image_test_history.html (+15/-0) dashboard_app/templates/dashboard_app/index.html (+53/-5) dashboard_app/templates/dashboard_app/report_detail.html (+9/-0) dashboard_app/templates/dashboard_app/test_result_detail.html (+30/-13) dashboard_app/templates/dashboard_app/test_run_detail.html (+2/-2) dashboard_app/templatetags/call.py (+64/-0) dashboard_app/tests/__init__.py (+1/-1) dashboard_app/tests/other/dataview.py (+2/-5) dashboard_app/tests/other/xml_rpc.py (+0/-132) dashboard_app/tests/utils.py (+0/-1) dashboard_app/tests/views/redirects.py (+75/-0) dashboard_app/tests/views/test_run_detail_view.py (+0/-1) dashboard_app/urls.py (+6/-0) dashboard_app/views.py (+98/-17) dashboard_app/xmlrpc.py (+68/-34) doc/changes.rst (+39/-0) doc/index.rst (+1/-1) |
To merge this branch: | bzr merge lp:~linaro-validation/lava-dashboard/0.6-wip |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Linaro Validation Team | Pending | ||
Review via email: mp+68142@code.launchpad.net |
Commit message
Description of the change
Snapshot of my work-in-progress. The biggest new feature is make_stream() supporting all kids of personal, shared, private and public streams.
There are no unit tests yet, I need to figure out how to test DashboardAPI sensibly (I think the call context can help here, we could mock it) but still I wanted to put this out the door.
- 245. By Zygmunt Krynicki
-
Bump required version of lava-server to 0.3b5
- 246. By Zygmunt Krynicki
-
Bump version to 0.6b4
- 250. By Zygmunt Krynicki
-
Add redirects to bundle, test run and test result
- 251. By Zygmunt Krynicki
-
Fix links to test run UUID in tables generated with jquery.dashboard.js
- 252. By Zygmunt Krynicki
-
Add fist approximation of the dashboard front page text
- 253. By Zygmunt Krynicki
-
Improve TestRun admin page
Display bundle and test, allow to filter by test
- 254. By Zygmunt Krynicki
-
Add ImageHealth model
This is not a real database backed model but a simple helper to build the new
LEB health/status pages. In the future it can be refactored to a real database
model backed by periodic de-normalized updates. - 255. By Zygmunt Krynicki
-
Add new LEB status/health views:
1) Page with matrix of links to rootfs / hwpack
2) Page showing status for all tests for a particular rootfs / hwpack
3) Page showing history of all the test runs for a particular rootfs / hwpack - 256. By Zygmunt Krynicki
-
Add templates for thee new LEB views
- 257. By Zygmunt Krynicki
-
Provide top-level link to LEB page
- 258. By Zygmunt Krynicki
-
Add special iframe-only mode to report detail pages
- 259. By Zygmunt Krynicki
-
Add {% call %} template tag.
This tag allows one to easily call arbitrary functions (including methods on
objects) with positional arguments. The there are two restrictions:1) The object, function or bound method must be in the context
2) Calling methods prefixed with _ is not allowed
Zygmunt Krynicki (zyga) wrote : | # |
Zygmunt Krynicki (zyga) wrote : | # |
Some more patches:
1) Added new dashboard front page
2) Improved TestRun admin page
- 260. By Zygmunt Krynicki
-
Only look at /anonymous/
lava-daily/ for LEB pages - 261. By Zygmunt Krynicki
-
Add a small border to make better distinction in LEB detail page
Paul Larson (pwlars) wrote : | # |
Agree, let's get this merged and deployed, but not sure if we want to call the view "LEB's". Maybe image status? something like that?
- 262. By Zygmunt Krynicki
-
Add DataView.__repr__
- 263. By Zygmunt Krynicki
-
Fix ImageHealth not to crash on missing 'android' rootfs
- 264. By Zygmunt Krynicki
-
Make DataViews use the same repository classes as DataReports
- 265. By Zygmunt Krynicki
-
Reformat data_view_detail breadcrumb for easier reading
- 266. By Zygmunt Krynicki
-
Some PEP8 fixes
- 267. By Zygmunt Krynicki
-
Add BaseContentHand
ler.characters( ) - 268. By Zygmunt Krynicki
-
Clean up _DataReportHandler
- 269. By Zygmunt Krynicki
-
Clean up _DataViewHandler and DataViewRepostiory
This code mainly goes to match the DataReportRepos
itory - 270. By Zygmunt Krynicki
-
Clean up DataView
- make DataView.name a normal accessor, not a property
- add get_absolute_url()
- add __unicode__ - 271. By Zygmunt Krynicki
-
Clean up DataReport
- reorder methods to look nicer
- add __repr__ - 272. By Zygmunt Krynicki
-
Update DataView tests to match code changes
- 273. By Zygmunt Krynicki
-
Use DataView.
get_absolute_ url() instead of {% url %} - 274. By Zygmunt Krynicki
-
Add missing import
- 275. By Zygmunt Krynicki
-
Fix some pyflakes (missing and unused imports)
- 276. By Zygmunt Krynicki
-
Remove the unused xml-rpc dispatcher
- 277. By Zygmunt Krynicki
-
Merge fixes and tests for bundle deserializer issue LP: 813567
- 278. By Zygmunt Krynicki
-
Remove obsolete dispatcher tests
- 279. By Zygmunt Krynicki
-
Rename LEBs to Image Status
- 280. By Zygmunt Krynicki
-
Use xml_rpc_signature from linaro_
django_ xmlrpc - 281. By Zygmunt Krynicki
-
Link to official documentation
- 282. By Zygmunt Krynicki
-
Add changelog for 0.6
Preview Diff
1 | === modified file 'dashboard_app/admin.py' |
2 | --- dashboard_app/admin.py 2011-04-29 11:54:24 +0000 |
3 | +++ dashboard_app/admin.py 2011-07-22 01:34:34 +0000 |
4 | @@ -137,8 +137,13 @@ |
5 | class TestRunAdmin(admin.ModelAdmin): |
6 | class NamedAttributeInline(generic.GenericTabularInline): |
7 | model = NamedAttribute |
8 | - list_display = ('analyzer_assigned_uuid', |
9 | - 'analyzer_assigned_date', 'import_assigned_date') |
10 | + list_filter = ('test'), |
11 | + list_display = ( |
12 | + 'test', |
13 | + 'analyzer_assigned_uuid', |
14 | + 'bundle', |
15 | + 'analyzer_assigned_date', |
16 | + 'import_assigned_date') |
17 | inlines = [NamedAttributeInline] |
18 | |
19 | |
20 | |
21 | === removed file 'dashboard_app/dataview.py' |
22 | --- dashboard_app/dataview.py 2011-07-09 15:09:48 +0000 |
23 | +++ dashboard_app/dataview.py 1970-01-01 00:00:00 +0000 |
24 | @@ -1,284 +0,0 @@ |
25 | -# Copyright (C) 2011 Linaro Limited |
26 | -# |
27 | -# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> |
28 | -# |
29 | -# This file is part of Launch Control. |
30 | -# |
31 | -# Launch Control is free software: you can redistribute it and/or modify |
32 | -# it under the terms of the GNU Affero General Public License version 3 |
33 | -# as published by the Free Software Foundation |
34 | -# |
35 | -# Launch Control is distributed in the hope that it will be useful, |
36 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
37 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
38 | -# GNU General Public License for more details. |
39 | -# |
40 | -# You should have received a copy of the GNU Affero General Public License |
41 | -# along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
42 | - |
43 | -""" |
44 | -DataViews: Encapsulated SQL query definitions. |
45 | - |
46 | -Implementation of the following launchpad blueprint: |
47 | -https://blueprints.launchpad.net/launch-control/+spec/other-linaro-n-data-views-for-launch-control |
48 | -""" |
49 | - |
50 | -from contextlib import closing |
51 | -from xml.sax import parseString |
52 | -from xml.sax.handler import ContentHandler |
53 | -import logging |
54 | -import os |
55 | -import re |
56 | - |
57 | - |
58 | -class DataView(object): |
59 | - """ |
60 | - Data view, a container for SQL query and optional arguments |
61 | - """ |
62 | - |
63 | - def __init__(self, name, backend_queries, arguments, documentation, summary): |
64 | - self.name = name |
65 | - self.backend_queries = backend_queries |
66 | - self.arguments = arguments |
67 | - self.documentation = documentation |
68 | - self.summary = summary |
69 | - |
70 | - def _get_connection_backend_name(self, connection): |
71 | - backend = str(type(connection)) |
72 | - if "sqlite" in backend: |
73 | - return "sqlite" |
74 | - elif "postgresql" in backend: |
75 | - return "postgresql" |
76 | - else: |
77 | - return "" |
78 | - |
79 | - def get_backend_specific_query(self, connection): |
80 | - """ |
81 | - Return BackendSpecificQuery for the specified connection |
82 | - """ |
83 | - sql_backend_name = self._get_connection_backend_name(connection) |
84 | - try: |
85 | - return self.backend_queries[sql_backend_name] |
86 | - except KeyError: |
87 | - return self.backend_queries.get(None, None) |
88 | - |
89 | - def lookup_argument(self, name): |
90 | - """ |
91 | - Return Argument with the specified name |
92 | - |
93 | - Raises LookupError if the argument cannot be found |
94 | - """ |
95 | - for argument in self.arguments: |
96 | - if argument.name == name: |
97 | - return argument |
98 | - raise LookupError(name) |
99 | - |
100 | - @classmethod |
101 | - def load_from_xml(self, xml_string): |
102 | - """ |
103 | - Load a data view instance from XML description |
104 | - |
105 | - This raises ValueError in several error situations. |
106 | - TODO: check what kind of exceptions this can raise |
107 | - """ |
108 | - handler = _DataViewHandler() |
109 | - parseString(xml_string, handler) |
110 | - return handler.data_view |
111 | - |
112 | - @classmethod |
113 | - def get_connection(cls): |
114 | - """ |
115 | - Get the appropriate connection for data views |
116 | - """ |
117 | - from django.db import connection, connections |
118 | - from django.db.utils import ConnectionDoesNotExist |
119 | - try: |
120 | - return connections['dataview'] |
121 | - except ConnectionDoesNotExist: |
122 | - logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed") |
123 | - return connection # NOTE: it's connection not connectionS (the default connection) |
124 | - |
125 | - def __call__(self, connection, **arguments): |
126 | - # Check if arguments have any bogus names |
127 | - valid_arg_names = frozenset([argument.name for argument in self.arguments]) |
128 | - for arg_name in arguments: |
129 | - if arg_name not in valid_arg_names: |
130 | - raise TypeError("Data view %s has no argument %r" % (self.name, arg_name)) |
131 | - # Get the SQL template for our database connection |
132 | - query = self.get_backend_specific_query(connection) |
133 | - if query is None: |
134 | - raise LookupError("Specified data view has no SQL implementation " |
135 | - "for current database") |
136 | - # Replace SQL aruments with django placeholders (connection agnostic) |
137 | - template = query.sql_template |
138 | - template = template.replace("%", "%%") |
139 | - # template = template.replace("{", "{{").replace("}", "}}") |
140 | - sql = template.format( |
141 | - **dict([ |
142 | - (arg_name, "%s") |
143 | - for arg_name in query.argument_list])) |
144 | - # Construct argument list using defaults for missing values |
145 | - sql_args = [ |
146 | - arguments.get(arg_name, self.lookup_argument(arg_name).default) |
147 | - for arg_name in query.argument_list] |
148 | - with closing(connection.cursor()) as cursor: |
149 | - # Execute the query with the specified arguments |
150 | - cursor.execute(sql, sql_args) |
151 | - # Get and return the results |
152 | - rows = cursor.fetchall() |
153 | - columns = cursor.description |
154 | - return rows, columns |
155 | - |
156 | - |
157 | -class Argument(object): |
158 | - """ |
159 | - Data view argument for SQL prepared statements |
160 | - """ |
161 | - |
162 | - def __init__(self, name, type, default, help): |
163 | - self.name = name |
164 | - self.type = type |
165 | - self.default = default |
166 | - self.help = help |
167 | - |
168 | - |
169 | -class BackendSpecificQuery(object): |
170 | - """ |
171 | - Backend-specific query and argument list |
172 | - """ |
173 | - |
174 | - def __init__(self, backend, sql_template, argument_list): |
175 | - self.backend = backend |
176 | - self.sql_template = sql_template |
177 | - self.argument_list = argument_list |
178 | - |
179 | - |
180 | -class _DataViewHandler(ContentHandler): |
181 | - """ |
182 | - ContentHandler subclass for parsing DataView documents |
183 | - """ |
184 | - |
185 | - def _end_text(self): |
186 | - """ |
187 | - Stop collecting text and produce a stripped string with deduplicated whitespace |
188 | - """ |
189 | - full_text = re.sub("\s+", " ", u''.join(self._text)).strip() |
190 | - self.text = None |
191 | - return full_text |
192 | - |
193 | - def _start_text(self): |
194 | - """ |
195 | - Start collecting text |
196 | - """ |
197 | - self._text = [] |
198 | - |
199 | - def startDocument(self): |
200 | - # Text can be None or a [] that accumulates all detected text |
201 | - self._text = None |
202 | - # Data view object |
203 | - self.data_view = DataView(None, {}, [], None, None) |
204 | - # Internal variables |
205 | - self._current_backend_query = None |
206 | - |
207 | - def endDocument(self): |
208 | - # TODO: check if we have anything defined |
209 | - if self.data_view.name is None: |
210 | - raise ValueError("No data view definition found") |
211 | - |
212 | - def startElement(self, name, attrs): |
213 | - if name == "data-view": |
214 | - self.data_view.name = attrs["name"] |
215 | - elif name == "summary" or name == "documentation": |
216 | - self._start_text() |
217 | - elif name == "sql": |
218 | - self._start_text() |
219 | - self._current_backend_query = BackendSpecificQuery(attrs.get("backend"), None, []) |
220 | - self.data_view.backend_queries[self._current_backend_query.backend] = self._current_backend_query |
221 | - elif name == "value": |
222 | - if "name" not in attrs: |
223 | - raise ValueError("<value> requires attribute 'name'") |
224 | - self._text.append("{" + attrs["name"] + "}") |
225 | - self._current_backend_query.argument_list.append(attrs["name"]) |
226 | - elif name == "argument": |
227 | - if "name" not in attrs: |
228 | - raise ValueError("<argument> requires attribute 'name'") |
229 | - if "type" not in attrs: |
230 | - raise ValueError("<argument> requires attribute 'type'") |
231 | - if attrs["type"] not in ("string", "number", "boolean", "timestamp"): |
232 | - raise ValueError("invalid value for argument 'type' on <argument>") |
233 | - argument = Argument(name=attrs["name"], type=attrs["type"], |
234 | - default=attrs.get("default", None), |
235 | - help=attrs.get("help", None)) |
236 | - self.data_view.arguments.append(argument) |
237 | - |
238 | - def endElement(self, name): |
239 | - if name == "sql": |
240 | - self._current_backend_query.sql_template = self._end_text() |
241 | - self._current_backend_query = None |
242 | - elif name == "documentation": |
243 | - self.data_view.documentation = self._end_text() |
244 | - elif name == "summary": |
245 | - self.data_view.summary = self._end_text() |
246 | - |
247 | - def characters(self, content): |
248 | - if isinstance(self._text, list): |
249 | - self._text.append(content) |
250 | - |
251 | - |
252 | -class DataViewRepository(object): |
253 | - |
254 | - _instance = None |
255 | - |
256 | - def __init__(self): |
257 | - self.data_views = [] |
258 | - |
259 | - def __iter__(self): |
260 | - return iter(self.data_views) |
261 | - |
262 | - def __getitem__(self, name): |
263 | - for item in self: |
264 | - if item.name == name: |
265 | - return item |
266 | - else: |
267 | - raise KeyError(name) |
268 | - |
269 | - def load_from_directory(self, directory): |
270 | - for name in os.listdir(directory): |
271 | - pathname = os.path.join(directory, name) |
272 | - if os.path.isfile(pathname) and pathname.endswith(".xml"): |
273 | - self.load_from_file(pathname) |
274 | - |
275 | - def load_from_file(self, pathname): |
276 | - try: |
277 | - with open(pathname, "rt") as stream: |
278 | - text = stream.read() |
279 | - data_view = DataView.load_from_xml(text) |
280 | - self.data_views.append(data_view) |
281 | - except Exception as exc: |
282 | - logging.error("Unable to load data view from %s: %s", pathname, exc) |
283 | - |
284 | - @classmethod |
285 | - def get_instance(cls): |
286 | - from django.conf import settings |
287 | - if cls._instance is None: |
288 | - cls._instance = cls() |
289 | - cls._instance.load_default() |
290 | - |
291 | - # I development mode always reload data views |
292 | - if getattr(settings, "DEBUG", False) is True: |
293 | - cls._instance.data_views = [] |
294 | - cls._instance.load_default() |
295 | - return cls._instance |
296 | - |
297 | - def load_default(self): |
298 | - from django.conf import settings |
299 | - for dirname in getattr(settings, "DATAVIEW_DIRS", []): |
300 | - self.load_from_directory(dirname) |
301 | - |
302 | - |
303 | -__all__ = [ |
304 | - "Argument", |
305 | - "BackendSpecificQuery", |
306 | - "DataView", |
307 | - "DataViewRepository", |
308 | -] |
309 | |
310 | === removed file 'dashboard_app/dispatcher.py' |
311 | --- dashboard_app/dispatcher.py 2011-05-07 23:08:30 +0000 |
312 | +++ dashboard_app/dispatcher.py 1970-01-01 00:00:00 +0000 |
313 | @@ -1,188 +0,0 @@ |
314 | -# Copyright (C) 2010 Linaro Limited |
315 | -# |
316 | -# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> |
317 | -# |
318 | -# This file is part of Launch Control. |
319 | -# |
320 | -# Launch Control is free software: you can redistribute it and/or modify |
321 | -# it under the terms of the GNU Affero General Public License version 3 |
322 | -# as published by the Free Software Foundation |
323 | -# |
324 | -# Launch Control is distributed in the hope that it will be useful, |
325 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
326 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
327 | -# GNU General Public License for more details. |
328 | -# |
329 | -# You should have received a copy of the GNU Affero General Public License |
330 | -# along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
331 | - |
332 | -""" |
333 | -XML-RPC Dispatcher for the dashboard |
334 | -""" |
335 | -import SimpleXMLRPCServer |
336 | -import logging |
337 | -import sys |
338 | -import xmlrpclib |
339 | - |
340 | -class _NullHandler(logging.Handler): |
341 | - def emit(self, record): |
342 | - pass |
343 | - |
344 | - |
345 | -class FaultCodes: |
346 | - """ |
347 | - Common fault codes. |
348 | - |
349 | - See: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php |
350 | - """ |
351 | - class ParseError: |
352 | - NOT_WELL_FORMED = -32700 |
353 | - UNSUPPORTED_ENCODING = -32701 |
354 | - INVALID_CHARACTER_FOR_ENCODING = -32702 |
355 | - class ServerError: |
356 | - INVALID_XML_RPC = -32600 |
357 | - REQUESTED_METHOD_NOT_FOUND = -32601 |
358 | - INVALID_METHOD_PARAMETERS = -32602 |
359 | - INTERNAL_XML_RPC_ERROR = -32603 |
360 | - APPLICATION_ERROR = -32500 |
361 | - SYSTEM_ERROR = -32400 |
362 | - TRANSPORT_ERROR = -32300 |
363 | - |
364 | - |
365 | -class DjangoXMLRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): |
366 | - """ |
367 | - Slightly extended XML-RPC dispatcher class suitable for embedding in |
368 | - django applications. |
369 | - """ |
370 | - #TODO: Implement _marshaled_dispatch() and capture XML errors to |
371 | - # translate them to appropriate standardized fault codes. There |
372 | - # might be some spill to the view code to make this complete. |
373 | - |
374 | - #TODO: Implement and expose system.getCapabilities() and advertise |
375 | - # support for standardised fault codes. |
376 | - # See: http://tech.groups.yahoo.com/group/xml-rpc/message/2897 |
377 | - def __init__(self): |
378 | - # it's a classic class, no super |
379 | - SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, |
380 | - allow_none=True) |
381 | - self.logger = logging.getLogger(__name__) |
382 | - self.logger.addHandler(_NullHandler()) |
383 | - |
384 | - def _lookup_func(self, method): |
385 | - """ |
386 | - Lookup implementation of method named `method`. |
387 | - |
388 | - Returns implementation of `method` or None if the method is not |
389 | - registered anywhere in the dispatcher. |
390 | - |
391 | - This new function is taken directly out of the base class |
392 | - implementation of _dispatch. The point is to be able to |
393 | - detect a situation where method is not known and return |
394 | - appropriate XML-RPC fault. With plain dispatcher it is |
395 | - not possible as the implementation raises a plain Exception |
396 | - to signal this error condition and capturing and interpreting |
397 | - arbitrary exceptions is flaky. |
398 | - """ |
399 | - func = None |
400 | - try: |
401 | - # check to see if a matching function has been registered |
402 | - func = self.funcs[method] |
403 | - except KeyError: |
404 | - if self.instance is not None: |
405 | - # check for a _dispatch method |
406 | - if hasattr(self.instance, '_dispatch'): |
407 | - # FIXME: pyflakes, params is undefined |
408 | - return self.instance._dispatch(method, params) |
409 | - else: |
410 | - # call instance method directly |
411 | - try: |
412 | - func = SimpleXMLRPCServer.resolve_dotted_attribute( |
413 | - self.instance, |
414 | - method, |
415 | - self.allow_dotted_names |
416 | - ) |
417 | - except AttributeError: |
418 | - pass |
419 | - return func |
420 | - |
421 | - def _dispatch(self, method, params): |
422 | - """ |
423 | - Improved dispatch method from the base dispatcher. |
424 | - |
425 | - The primary improvement is exception handling: |
426 | - - xml-rpc faults are passed back to the caller |
427 | - - missing methods return standardized fault code ( |
428 | - FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND) |
429 | - - all other exceptions in the called method are translated |
430 | - to standardized internal xml-rpc fault code |
431 | - (FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR). In |
432 | - addition such errors cause _report_incident() to be |
433 | - called. This allows to hook a notification mechanism for |
434 | - deployed servers where exceptions are, for example, mailed |
435 | - to the administrator. |
436 | - """ |
437 | - func = self._lookup_func(method) |
438 | - if func is None: |
439 | - raise xmlrpclib.Fault( |
440 | - FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND, |
441 | - "No such method: %r" % method) |
442 | - try: |
443 | - # TODO: check parameter types before calling |
444 | - return func(*params) |
445 | - except xmlrpclib.Fault: |
446 | - # Forward XML-RPC Faults to the client |
447 | - raise |
448 | - except: |
449 | - # Treat all other exceptions as internal errors |
450 | - # This prevents the clients from seeing internals |
451 | - exc_type, exc_value, exc_tb = sys.exc_info() |
452 | - incident_id = self._report_incident(method, params, exc_type, exc_value, exc_tb) |
453 | - string = ("Dashboard has encountered internal error. " |
454 | - "Incident ID is: %s" % (incident_id,)) |
455 | - raise xmlrpclib.Fault( |
456 | - FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR, |
457 | - string) |
458 | - |
459 | - def _report_incident(self, method, params, exc_type, exc_value, exc_tb): |
460 | - """ |
461 | - Report an exception that happened |
462 | - """ |
463 | - self.logger.exception("Internal error when dispatching " |
464 | - "XML-RPC method: %s%r", method, params) |
465 | - # TODO: store the exception somewhere and assign fault codes |
466 | - return None |
467 | - |
468 | - def system_methodSignature(self, method): |
469 | - if method.startswith("_"): |
470 | - return "" |
471 | - if self.instance is not None: |
472 | - func = getattr(self.instance, method, None) |
473 | - else: |
474 | - func = self.funcs.get(method) |
475 | - # When function is not known return empty string |
476 | - if func is None: |
477 | - return "" |
478 | - # When signature is not known return "undef" |
479 | - # See: http://xmlrpc-c.sourceforge.net/introspection.html |
480 | - return getattr(func, 'xml_rpc_signature', "undef") |
481 | - |
482 | - |
483 | -def xml_rpc_signature(*sig): |
484 | - """ |
485 | - Small helper that attaches "xml_rpc_signature" attribute to the |
486 | - function. The attribute is a list of values that is then reported |
487 | - by system_methodSignature(). |
488 | - |
489 | - This is a simplification of the XML-RPC spec that allows to attach a |
490 | - list of variants (like I may accept this set of arguments, or that |
491 | - set or that other one). This version has only one set of arguments. |
492 | - |
493 | - Note that it's a purely presentational argument for our |
494 | - implementation. Putting bogus values here won't spoil the day. |
495 | - |
496 | - The first element is the signature of the return type. |
497 | - """ |
498 | - def decorator(func): |
499 | - func.xml_rpc_signature = sig |
500 | - return func |
501 | - return decorator |
502 | |
503 | === modified file 'dashboard_app/models.py' |
504 | --- dashboard_app/models.py 2011-07-12 14:38:26 +0000 |
505 | +++ dashboard_app/models.py 2011-07-22 01:34:34 +0000 |
506 | @@ -26,6 +26,7 @@ |
507 | import os |
508 | import simplejson |
509 | import traceback |
510 | +import contextlib |
511 | |
512 | from django.contrib.auth.models import User |
513 | from django.contrib.contenttypes import generic |
514 | @@ -43,6 +44,7 @@ |
515 | from dashboard_app.managers import BundleManager |
516 | from dashboard_app.repositories import RepositoryItem |
517 | from dashboard_app.repositories.data_report import DataReportRepository |
518 | +from dashboard_app.repositories.data_view import DataViewRepository |
519 | |
520 | # Fix some django issues we ran into |
521 | from dashboard_app.patches import patch |
522 | @@ -364,6 +366,9 @@ |
523 | def get_absolute_url(self): |
524 | return ("dashboard_app.views.bundle_detail", [self.bundle_stream.pathname, self.content_sha1]) |
525 | |
526 | + def get_permalink(self): |
527 | + return reverse("dashboard_app.views.redirect_to_bundle", args=[self.content_sha1]) |
528 | + |
529 | def save(self, *args, **kwargs): |
530 | if self.content: |
531 | try: |
532 | @@ -749,6 +754,9 @@ |
533 | self.bundle.content_sha1, |
534 | self.analyzer_assigned_uuid]) |
535 | |
536 | + def get_permalink(self): |
537 | + return reverse("dashboard_app.views.redirect_to_test_run", args=[self.analyzer_assigned_uuid]) |
538 | + |
539 | def get_summary_results(self): |
540 | stats = self.test_results.values('result').annotate( |
541 | count=models.Count('result')).order_by() |
542 | @@ -932,6 +940,20 @@ |
543 | def __unicode__(self): |
544 | return "Result {0}/{1}".format(self.test_run.analyzer_assigned_uuid, self.relative_index) |
545 | |
546 | + @models.permalink |
547 | + def get_absolute_url(self): |
548 | + return ("dashboard_app.views.test_result_detail", [ |
549 | + self.test_run.bundle.bundle_stream.pathname, |
550 | + self.test_run.bundle.content_sha1, |
551 | + self.test_run.analyzer_assigned_uuid, |
552 | + self.relative_index, |
553 | + ]) |
554 | + |
555 | + def get_permalink(self): |
556 | + return reverse("dashboard_app.views.redirect_to_test_result", |
557 | + args=[self.test_run.analyzer_assigned_uuid, |
558 | + self.relative_index]) |
559 | + |
560 | @property |
561 | def result_code(self): |
562 | """ |
563 | @@ -973,15 +995,6 @@ |
564 | |
565 | duration = property(_get_duration, _set_duration) |
566 | |
567 | - @models.permalink |
568 | - def get_absolute_url(self): |
569 | - return ("dashboard_app.views.test_result_detail", [ |
570 | - self.test_run.bundle.bundle_stream.pathname, |
571 | - self.test_run.bundle.content_sha1, |
572 | - self.test_run.analyzer_assigned_uuid, |
573 | - self.relative_index, |
574 | - ]) |
575 | - |
576 | def related_attachment_available(self): |
577 | """ |
578 | Check if there is a log file attached to the test run that has |
579 | @@ -1001,6 +1014,105 @@ |
580 | order_with_respect_to = 'test_run' |
581 | |
582 | |
583 | +class DataView(RepositoryItem): |
584 | + """ |
585 | + Data view, a container for SQL query and optional arguments |
586 | + """ |
587 | + |
588 | + repository = DataViewRepository() |
589 | + |
590 | + def __init__(self, name, backend_queries, arguments, documentation, summary): |
591 | + self.name = name |
592 | + self.backend_queries = backend_queries |
593 | + self.arguments = arguments |
594 | + self.documentation = documentation |
595 | + self.summary = summary |
596 | + |
597 | + def __unicode__(self): |
598 | + return self.name |
599 | + |
600 | + def __repr__(self): |
601 | + return "<DataView name=%r>" % (self.name,) |
602 | + |
603 | + @models.permalink |
604 | + def get_absolute_url(self): |
605 | + return ("dashboard_app.views.data_view_detail", [self.name]) |
606 | + |
607 | + def _get_connection_backend_name(self, connection): |
608 | + backend = str(type(connection)) |
609 | + if "sqlite" in backend: |
610 | + return "sqlite" |
611 | + elif "postgresql" in backend: |
612 | + return "postgresql" |
613 | + else: |
614 | + return "" |
615 | + |
616 | + def get_backend_specific_query(self, connection): |
617 | + """ |
618 | + Return BackendSpecificQuery for the specified connection |
619 | + """ |
620 | + sql_backend_name = self._get_connection_backend_name(connection) |
621 | + try: |
622 | + return self.backend_queries[sql_backend_name] |
623 | + except KeyError: |
624 | + return self.backend_queries.get(None, None) |
625 | + |
626 | + def lookup_argument(self, name): |
627 | + """ |
628 | + Return Argument with the specified name |
629 | + |
630 | + Raises LookupError if the argument cannot be found |
631 | + """ |
632 | + for argument in self.arguments: |
633 | + if argument.name == name: |
634 | + return argument |
635 | + raise LookupError(name) |
636 | + |
637 | + @classmethod |
638 | + def get_connection(cls): |
639 | + """ |
640 | + Get the appropriate connection for data views |
641 | + """ |
642 | + from django.db import connection, connections |
643 | + from django.db.utils import ConnectionDoesNotExist |
644 | + try: |
645 | + return connections['dataview'] |
646 | + except ConnectionDoesNotExist: |
647 | + logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed") |
648 | + return connection # NOTE: it's connection not connectionS (the default connection) |
649 | + |
650 | + def __call__(self, connection, **arguments): |
651 | + # Check if arguments have any bogus names |
652 | + valid_arg_names = frozenset([argument.name for argument in self.arguments]) |
653 | + for arg_name in arguments: |
654 | + if arg_name not in valid_arg_names: |
655 | + raise TypeError("Data view %s has no argument %r" % (self.name, arg_name)) |
656 | + # Get the SQL template for our database connection |
657 | + query = self.get_backend_specific_query(connection) |
658 | + if query is None: |
659 | + raise LookupError("Specified data view has no SQL implementation " |
660 | + "for current database") |
661 | + # Replace SQL aruments with django placeholders (connection agnostic) |
662 | + template = query.sql_template |
663 | + template = template.replace("%", "%%") |
664 | + # template = template.replace("{", "{{").replace("}", "}}") |
665 | + sql = template.format( |
666 | + **dict([ |
667 | + (arg_name, "%s") |
668 | + for arg_name in query.argument_list])) |
669 | + # Construct argument list using defaults for missing values |
670 | + sql_args = [ |
671 | + arguments.get(arg_name, self.lookup_argument(arg_name).default) |
672 | + for arg_name in query.argument_list] |
673 | + with contextlib.closing(connection.cursor()) as cursor: |
674 | + # Execute the query with the specified arguments |
675 | + cursor.execute(sql, sql_args) |
676 | + # Get and return the results |
677 | + rows = cursor.fetchall() |
678 | + columns = cursor.description |
679 | + return rows, columns |
680 | + |
681 | + |
682 | class DataReport(RepositoryItem): |
683 | """ |
684 | Data reports are small snippets of xml that define |
685 | @@ -1013,6 +1125,16 @@ |
686 | self._html = None |
687 | self._data = kwargs |
688 | |
689 | + def __unicode__(self): |
690 | + return self.title |
691 | + |
692 | + def __repr__(self): |
693 | + return "<DataReport name=%r>" % (self.name,) |
694 | + |
695 | + @models.permalink |
696 | + def get_absolute_url(self): |
697 | + return ("dashboard_app.views.report_detail", [self.name]) |
698 | + |
699 | def _get_raw_html(self): |
700 | pathname = os.path.join(self.base_path, self.path) |
701 | try: |
702 | @@ -1041,13 +1163,6 @@ |
703 | self._html = template.render(context) |
704 | return self._html |
705 | |
706 | - def __unicode__(self): |
707 | - return self.title |
708 | - |
709 | - @models.permalink |
710 | - def get_absolute_url(self): |
711 | - return ("dashboard_app.views.report_detail", [self.name]) |
712 | - |
713 | @property |
714 | def title(self): |
715 | return self._data['title'] |
716 | @@ -1067,3 +1182,90 @@ |
717 | @property |
718 | def author(self): |
719 | return self._data.get('author') |
720 | + |
721 | + |
722 | +class ImageHealth(object): |
723 | + |
724 | + def __init__(self, rootfs_type, hwpack_type): |
725 | + self.rootfs_type = rootfs_type |
726 | + self.hwpack_type = hwpack_type |
727 | + |
728 | + @models.permalink |
729 | + def get_absolute_url(self): |
730 | + return ("dashboard_app.views.image_status_detail", [ |
731 | + self.rootfs_type, self.hwpack_type]) |
732 | + |
733 | + def get_tests(self): |
734 | + return Test.objects.filter(test_runs__in=self.get_test_runs()).distinct() |
735 | + |
736 | + def current_health_for_test(self, test): |
737 | + test_run = self.get_current_test_run(test) |
738 | + test_results = test_run.test_results |
739 | + fail_count = test_results.filter( |
740 | + result=TestResult.RESULT_FAIL).count() |
741 | + pass_count = test_results.filter( |
742 | + result=TestResult.RESULT_PASS).count() |
743 | + total_count = test_results.count() |
744 | + return { |
745 | + "test_run": test_run, |
746 | + "total_count": total_count, |
747 | + "fail_count": fail_count, |
748 | + "pass_count": pass_count, |
749 | + "other_count": total_count - (fail_count + pass_count), |
750 | + "fail_percent": fail_count * 100.0 / total_count if total_count > 0 else None, |
751 | + } |
752 | + |
753 | + def overall_health_for_test(self, test): |
754 | + test_run_list = self.get_all_test_runs_for_test(test) |
755 | + test_result_list = TestResult.objects.filter(test_run__in=test_run_list) |
756 | + fail_result_list = test_result_list.filter(result=TestResult.RESULT_FAIL) |
757 | + pass_result_list = test_result_list.filter(result=TestResult.RESULT_PASS) |
758 | + |
759 | + total_count = test_result_list.count() |
760 | + fail_count = fail_result_list.count() |
761 | + pass_count = pass_result_list.count() |
762 | + fail_percent = fail_count * 100.0 / total_count if total_count > 0 else None |
763 | + return { |
764 | + "total_count": total_count, |
765 | + "total_run_count": test_run_list.count(), |
766 | + "fail_count": fail_count, |
767 | + "pass_count": pass_count, |
768 | + "other_count": total_count - fail_count - pass_count, |
769 | + "fail_percent": fail_percent, |
770 | + } |
771 | + |
772 | + def get_test_runs(self): |
773 | + return TestRun.objects.filter( |
774 | + bundle__bundle_stream__pathname="/anonymous/lava-daily/" |
775 | + ).filter( |
776 | + attributes__name='rootfs.type', |
777 | + attributes__value=self.rootfs_type |
778 | + ).filter( |
779 | + attributes__name='hwpack.type', |
780 | + attributes__value=self.hwpack_type) |
781 | + |
782 | + def get_current_test_run(self, test): |
783 | + return self.get_all_test_runs_for_test(test).order_by('-analyzer_assigned_date')[0] |
784 | + |
785 | + def get_all_test_runs_for_test(self, test): |
786 | + return self.get_test_runs().filter(test=test) |
787 | + |
788 | + @classmethod |
789 | + def get_rootfs_list(self): |
790 | + rootfs_list = [ |
791 | + attr['value'] |
792 | + for attr in NamedAttribute.objects.filter( |
793 | + name='rootfs.type').values('value').distinct()] |
794 | + try: |
795 | + rootfs_list.remove('android') |
796 | + except ValueError: |
797 | + pass |
798 | + return rootfs_list |
799 | + |
800 | + @classmethod |
801 | + def get_hwpack_list(self): |
802 | + hwpack_list = [ |
803 | + attr['value'] |
804 | + for attr in NamedAttribute.objects.filter( |
805 | + name='hwpack.type').values('value').distinct()] |
806 | + return hwpack_list |
807 | |
808 | === modified file 'dashboard_app/repositories/__init__.py' |
809 | --- dashboard_app/repositories/__init__.py 2011-05-03 22:07:19 +0000 |
810 | +++ dashboard_app/repositories/__init__.py 2011-07-22 01:34:34 +0000 |
811 | @@ -32,7 +32,8 @@ |
812 | """ |
813 | |
814 | def __new__(mcls, name, bases, namespace): |
815 | - cls = super(RepositoryItemMeta, mcls).__new__(mcls, name, bases, namespace) |
816 | + cls = super(RepositoryItemMeta, mcls).__new__( |
817 | + mcls, name, bases, namespace) |
818 | if "repository" in namespace: |
819 | repo = cls.repository |
820 | repo.item_cls = cls |
821 | @@ -46,7 +47,7 @@ |
822 | Each repository item is loaded from a XML file. |
823 | """ |
824 | |
825 | - __metaclass__ = RepositoryItemMeta |
826 | + __metaclass__ = RepositoryItemMeta |
827 | |
828 | _base_path = None |
829 | |
830 | @@ -57,7 +58,6 @@ |
831 | def base_path(self): |
832 | return self._base_path |
833 | |
834 | - |
835 | class DoesNotExist(Exception): |
836 | pass |
837 | |
838 | @@ -89,12 +89,12 @@ |
839 | |
840 | def all(self): |
841 | return self |
842 | - |
843 | + |
844 | def get(self, **kwargs): |
845 | query = self.filter(**kwargs) |
846 | if len(query) == 1: |
847 | return query[0] |
848 | - if not query: |
849 | + if not query: |
850 | raise self.model.DoesNotExist() |
851 | else: |
852 | raise self.model.MultipleValuesReturned() |
853 | @@ -127,7 +127,7 @@ |
854 | __metaclass__ = abc.ABCMeta |
855 | |
856 | def __init__(self): |
857 | - self.item_cls = None # later patched by RepositoryItemMeta |
858 | + self.item_cls = None # later patched by RepositoryItemMeta |
859 | self._items = [] |
860 | self._did_load = False |
861 | |
862 | @@ -154,9 +154,10 @@ |
863 | try: |
864 | items = os.listdir(directory) |
865 | except (OSError, IOError) as exc: |
866 | - logging.exception("Unable to enumreate directory: %s: %s", directory, exc) |
867 | + logging.exception("Unable to enumreate directory: %s: %s", |
868 | + directory, exc) |
869 | else: |
870 | - for name in items: |
871 | + for name in items: |
872 | pathname = os.path.join(directory, name) |
873 | if os.path.isfile(pathname) and pathname.endswith(".xml"): |
874 | self.load_from_file(pathname) |
875 | @@ -176,7 +177,8 @@ |
876 | item._load_from_external_representation(pathname) |
877 | self._items.append(item) |
878 | except Exception as exc: |
879 | - logging.exception("Unable to load object into repository %s: %s", pathname, exc) |
880 | + logging.exception("Unable to load object into repository %s: %s", |
881 | + pathname, exc) |
882 | |
883 | @abc.abstractproperty |
884 | def settings_variable(self): |
885 | |
886 | === added file 'dashboard_app/repositories/common.py' |
887 | --- dashboard_app/repositories/common.py 1970-01-01 00:00:00 +0000 |
888 | +++ dashboard_app/repositories/common.py 2011-07-22 01:34:34 +0000 |
889 | @@ -0,0 +1,47 @@ |
890 | +# Copyright (C) 2011 Linaro Limited |
891 | +# |
892 | +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> |
893 | +# |
894 | +# This file is part of Launch Control. |
895 | +# |
896 | +# Launch Control is free software: you can redistribute it and/or modify |
897 | +# it under the terms of the GNU Affero General Public License version 3 |
898 | +# as published by the Free Software Foundation |
899 | +# |
900 | +# Launch Control is distributed in the hope that it will be useful, |
901 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
902 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
903 | +# GNU General Public License for more details. |
904 | +# |
905 | +# You should have received a copy of the GNU Affero General Public License |
906 | +# along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
907 | + |
908 | + |
909 | +from xml.sax.handler import ContentHandler |
910 | +import re |
911 | + |
912 | + |
913 | +class BaseContentHandler(ContentHandler): |
914 | + |
915 | + def _end_text(self): |
916 | + """ |
917 | + Stop collecting text and produce a stripped string with de-duplicated |
918 | + whitespace |
919 | + """ |
920 | + full_text = re.sub("\s+", " ", u''.join(self._text)).strip() |
921 | + self.text = None |
922 | + return full_text |
923 | + |
924 | + def _start_text(self): |
925 | + """ |
926 | + Start collecting text |
927 | + """ |
928 | + self._text = [] |
929 | + |
930 | + def characters(self, content): |
931 | + if isinstance(self._text, list): |
932 | + self._text.append(content) |
933 | + |
934 | + def startDocument(self): |
935 | + # Text can be None or a [] that accumulates all detected text |
936 | + self._text = None |
937 | |
938 | === modified file 'dashboard_app/repositories/data_report.py' |
939 | --- dashboard_app/repositories/data_report.py 2011-05-03 18:16:04 +0000 |
940 | +++ dashboard_app/repositories/data_report.py 2011-07-22 01:34:34 +0000 |
941 | @@ -18,34 +18,19 @@ |
942 | |
943 | |
944 | from xml.sax import parseString |
945 | -from xml.sax.handler import ContentHandler |
946 | -import re |
947 | |
948 | from dashboard_app.repositories import Repository, Undefined, Object |
949 | - |
950 | - |
951 | -class _DataReportHandler(ContentHandler): |
952 | +from dashboard_app.repositories.common import BaseContentHandler |
953 | + |
954 | + |
955 | +class _DataReportHandler(BaseContentHandler): |
956 | """ |
957 | - ContentHandler subclass for parsing DataView documents |
958 | + ContentHandler subclass for parsing DataReport documents |
959 | """ |
960 | |
961 | - def _end_text(self): |
962 | - """ |
963 | - Stop collecting text and produce a stripped string with deduplicated whitespace |
964 | - """ |
965 | - full_text = re.sub("\s+", " ", u''.join(self._text)).strip() |
966 | - self.text = None |
967 | - return full_text |
968 | - |
969 | - def _start_text(self): |
970 | - """ |
971 | - Start collecting text |
972 | - """ |
973 | - self._text = [] |
974 | - |
975 | def startDocument(self): |
976 | - # Text can be None or a [] that accumulates all detected text |
977 | - self._text = None |
978 | + # Classic-classes |
979 | + BaseContentHandler.startDocument(self) |
980 | # Data report object |
981 | self.obj = Object() |
982 | |
983 | @@ -70,10 +55,6 @@ |
984 | self.obj.title = self._end_text() |
985 | elif name == "path": |
986 | self.obj.path = self._end_text() |
987 | - |
988 | - def characters(self, content): |
989 | - if isinstance(self._text, list): |
990 | - self._text.append(content) |
991 | |
992 | |
993 | class DataReportRepository(Repository): |
994 | |
995 | === added file 'dashboard_app/repositories/data_view.py' |
996 | --- dashboard_app/repositories/data_view.py 1970-01-01 00:00:00 +0000 |
997 | +++ dashboard_app/repositories/data_view.py 2011-07-22 01:34:34 +0000 |
998 | @@ -0,0 +1,133 @@ |
999 | +# Copyright (C) 2011 Linaro Limited |
1000 | +# |
1001 | +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> |
1002 | +# |
1003 | +# This file is part of Launch Control. |
1004 | +# |
1005 | +# Launch Control is free software: you can redistribute it and/or modify |
1006 | +# it under the terms of the GNU Affero General Public License version 3 |
1007 | +# as published by the Free Software Foundation |
1008 | +# |
1009 | +# Launch Control is distributed in the hope that it will be useful, |
1010 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1011 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1012 | +# GNU General Public License for more details. |
1013 | +# |
1014 | +# You should have received a copy of the GNU Affero General Public License |
1015 | +# along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
1016 | + |
1017 | +""" |
1018 | +DataViews: Encapsulated SQL query definitions. |
1019 | + |
1020 | +Implementation of the following launchpad blueprint: |
1021 | +https://blueprints.launchpad.net/launch-control/+spec/other-linaro-n-data-views-for-launch-control |
1022 | +""" |
1023 | + |
1024 | + |
1025 | +from xml.sax import parseString |
1026 | + |
1027 | +from dashboard_app.repositories import Repository, Undefined, Object |
1028 | +from dashboard_app.repositories.common import BaseContentHandler |
1029 | + |
1030 | + |
1031 | +class _DataViewHandler(BaseContentHandler): |
1032 | + |
1033 | + """ |
1034 | + ContentHandler subclass for parsing DataView documents |
1035 | + """ |
1036 | + |
1037 | + def startDocument(self): |
1038 | + # Classic-classes |
1039 | + BaseContentHandler.startDocument(self) |
1040 | + # Data view object |
1041 | + self.obj = Object() |
1042 | + # Set default values |
1043 | + self.obj.name = Undefined |
1044 | + self.obj.backend_queries = {} |
1045 | + self.obj.arguments = [] |
1046 | + self.obj.documentation = None |
1047 | + self.obj.summary = None |
1048 | + # Internal variables |
1049 | + self._current_backend_query = None |
1050 | + |
1051 | + def endDocument(self): |
1052 | + # TODO: check if we have anything defined |
1053 | + if self.obj.name is Undefined: |
1054 | + raise ValueError("No data view definition found") |
1055 | + |
1056 | + def startElement(self, name, attrs): |
1057 | + if name == "data-view": |
1058 | + self.obj.name = attrs["name"] |
1059 | + elif name == "summary" or name == "documentation": |
1060 | + self._start_text() |
1061 | + elif name == "sql": |
1062 | + self._start_text() |
1063 | + self._current_backend_query = BackendSpecificQuery( |
1064 | + attrs.get("backend"), None, []) |
1065 | + self.obj.backend_queries[ |
1066 | + self._current_backend_query.backend] = self._current_backend_query |
1067 | + elif name == "value": |
1068 | + if "name" not in attrs: |
1069 | + raise ValueError("<value> requires attribute 'name'") |
1070 | + self._text.append("{" + attrs["name"] + "}") |
1071 | + self._current_backend_query.argument_list.append(attrs["name"]) |
1072 | + elif name == "argument": |
1073 | + if "name" not in attrs: |
1074 | + raise ValueError("<argument> requires attribute 'name'") |
1075 | + if "type" not in attrs: |
1076 | + raise ValueError("<argument> requires attribute 'type'") |
1077 | + if attrs["type"] not in ("string", "number", "boolean", "timestamp"): |
1078 | + raise ValueError("invalid value for argument 'type' on <argument>") |
1079 | + argument = Argument(name=attrs["name"], type=attrs["type"], |
1080 | + default=attrs.get("default", None), |
1081 | + help=attrs.get("help", None)) |
1082 | + self.obj.arguments.append(argument) |
1083 | + |
1084 | + def endElement(self, name): |
1085 | + if name == "sql": |
1086 | + self._current_backend_query.sql_template = self._end_text() |
1087 | + self._current_backend_query = None |
1088 | + elif name == "documentation": |
1089 | + self.obj.documentation = self._end_text() |
1090 | + elif name == "summary": |
1091 | + self.obj.summary = self._end_text() |
1092 | + |
1093 | + |
1094 | +class Argument(object): |
1095 | + """ |
1096 | + Data view argument for SQL prepared statements |
1097 | + """ |
1098 | + |
1099 | + def __init__(self, name, type, default, help): |
1100 | + self.name = name |
1101 | + self.type = type |
1102 | + self.default = default |
1103 | + self.help = help |
1104 | + |
1105 | + |
1106 | +class BackendSpecificQuery(object): |
1107 | + """ |
1108 | + Backend-specific query and argument list |
1109 | + """ |
1110 | + |
1111 | + def __init__(self, backend, sql_template, argument_list): |
1112 | + self.backend = backend |
1113 | + self.sql_template = sql_template |
1114 | + self.argument_list = argument_list |
1115 | + |
1116 | + |
1117 | +class DataViewRepository(Repository): |
1118 | + |
1119 | + @property |
1120 | + def settings_variable(self): |
1121 | + return "DATAVIEW_DIRS" |
1122 | + |
1123 | + def load_from_xml_string(self, text): |
1124 | + handler = _DataViewHandler() |
1125 | + parseString(text, handler) |
1126 | + return self.item_cls(**handler.obj.__dict__) |
1127 | + |
1128 | + |
1129 | +__all__ = [ |
1130 | + "DataViewRepository", |
1131 | +] |
1132 | |
1133 | === modified file 'dashboard_app/static/js/jquery.dashboard.js' |
1134 | --- dashboard_app/static/js/jquery.dashboard.js 2011-07-09 15:09:19 +0000 |
1135 | +++ dashboard_app/static/js/jquery.dashboard.js 2011-07-22 01:34:34 +0000 |
1136 | @@ -76,7 +76,7 @@ |
1137 | } |
1138 | if (column.name == "UUID") { |
1139 | /* This is a bit hacky but will work for now */ |
1140 | - cell_link = _url + ".." + "/test-runs/" + cell + "/"; |
1141 | + cell_link = _url + ".." + "/permalink/test-run/" + cell + "/"; |
1142 | } |
1143 | html += "<td>"; |
1144 | if (cell_link) { |
1145 | |
1146 | === modified file 'dashboard_app/templates/dashboard_app/_extension_navigation.html' |
1147 | --- dashboard_app/templates/dashboard_app/_extension_navigation.html 2011-07-12 02:34:12 +0000 |
1148 | +++ dashboard_app/templates/dashboard_app/_extension_navigation.html 2011-07-22 01:34:34 +0000 |
1149 | @@ -1,6 +1,8 @@ |
1150 | {% load i18n %} |
1151 | <div id="lava-server-extension-navigation" class="lava-server-sub-toolbar"> |
1152 | <ul> |
1153 | + <li><a href="{% url dashboard_app.views.image_status_list %}" |
1154 | + >{% trans "Image Status" %}</a></li> |
1155 | <li><a href="{% url dashboard_app.views.bundle_stream_list %}" |
1156 | >{% trans "Bundle Streams" %}</a></li> |
1157 | <li><a href="{% url dashboard_app.views.test_list %}" |
1158 | |
1159 | === modified file 'dashboard_app/templates/dashboard_app/bundle_detail.html' |
1160 | --- dashboard_app/templates/dashboard_app/bundle_detail.html 2011-07-13 11:07:18 +0000 |
1161 | +++ dashboard_app/templates/dashboard_app/bundle_detail.html 2011-07-22 01:34:34 +0000 |
1162 | @@ -11,6 +11,20 @@ |
1163 | |
1164 | |
1165 | {% block content %} |
1166 | +<div class="ui-widget"> |
1167 | + <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em"> |
1168 | + <span |
1169 | + class="ui-icon ui-icon-info" |
1170 | + style="float: left; margin-right: 0.3em;"></span> |
1171 | + <strong>{% trans "Note:" %}</strong> |
1172 | + {% blocktrans %} |
1173 | + You can navigate to this bundle, regardless of the bundle stream it is |
1174 | + located in, by using this |
1175 | + {% endblocktrans %} |
1176 | + <a href="{{ bundle.get_permalink }}" >{% trans "permalink" %}</a> |
1177 | + </div> |
1178 | +</div> |
1179 | +<br/> |
1180 | <script type="text/javascript"> |
1181 | $(document).ready(function() { |
1182 | $("#tabs").tabs({ |
1183 | |
1184 | === modified file 'dashboard_app/templates/dashboard_app/data_view_list.html' |
1185 | --- dashboard_app/templates/dashboard_app/data_view_list.html 2011-07-12 02:34:12 +0000 |
1186 | +++ dashboard_app/templates/dashboard_app/data_view_list.html 2011-07-22 01:34:34 +0000 |
1187 | @@ -34,7 +34,7 @@ |
1188 | <tbody> |
1189 | {% for data_view in data_view_list %} |
1190 | <tr> |
1191 | - <td><a href="{% url dashboard_app.views.data_view_detail data_view.name %}">{{ data_view.name }}</a></td> |
1192 | + <td><a href="{{ data_view.get_absolute_url }}">{{ data_view.name }}</a></td> |
1193 | <td>{{ data_view.summary }}</td> |
1194 | </tr> |
1195 | {% endfor %} |
1196 | |
1197 | === added file 'dashboard_app/templates/dashboard_app/image_status_detail.html' |
1198 | --- dashboard_app/templates/dashboard_app/image_status_detail.html 1970-01-01 00:00:00 +0000 |
1199 | +++ dashboard_app/templates/dashboard_app/image_status_detail.html 2011-07-22 01:34:34 +0000 |
1200 | @@ -0,0 +1,71 @@ |
1201 | +{% extends "dashboard_app/_content.html" %} |
1202 | +{% load call %} |
1203 | + |
1204 | +{% block content %} |
1205 | +<script type="text/javascript" charset="utf-8"> |
1206 | + $(document).ready(function() { |
1207 | + oTable = $('#tests').dataTable({ |
1208 | + "bJQueryUI": true, |
1209 | + "bPaginate": false, |
1210 | + }); |
1211 | + }); |
1212 | +</script> |
1213 | +<table class="demo_jui display" id="tests"> |
1214 | + <thead> |
1215 | + <tr> |
1216 | + <th rowspan="2">Test</th> |
1217 | + <th colspan="5">Totals</th> |
1218 | + <th colspan="4" style="border-left: 1px solid black">Most Recent Test Run</th> |
1219 | + <th rowspan="2" style="width: 30%">Description</th> |
1220 | + </th> |
1221 | + <tr> |
1222 | + <th>PASS</th> |
1223 | + <th>FAIL</th> |
1224 | + <th>FAIL rate</th> |
1225 | + <th>Test Runs</th> |
1226 | + <th>Test Results</th> |
1227 | + <th>PASS</th> |
1228 | + <th>FAIL</th> |
1229 | + <th>FAIL rate</th> |
1230 | + <th>Test Results</th> |
1231 | + </tr> |
1232 | + </thead> |
1233 | + <tbody> |
1234 | + {% for test in image_health.get_tests %} |
1235 | + {% call image_health.current_health_for_test test as current_test_health %} |
1236 | + {% call image_health.overall_health_for_test test as overall_test_health %} |
1237 | + <tr |
1238 | + {% if current_test_health.fail_count > 0 %} |
1239 | + {% if current_test_health.pass_count == 0 %} |
1240 | + style="background-color: rgba(255, 0, 0, 0.5)" |
1241 | + {% else %} |
1242 | + style="background-color: rgba(255, 165, 0, 0.5)" |
1243 | + {% endif %} |
1244 | + {% else %} |
1245 | + {% if current_test_health.pass_count > 0 %} |
1246 | + style="background-color: rgba(173, 255, 47, 0.5)" |
1247 | + {% endif %} |
1248 | + {% endif %} |
1249 | + > |
1250 | + <td>{{ test.test_id }}</td> |
1251 | + <td>{{ overall_test_health.pass_count }}</td> |
1252 | + <td>{{ overall_test_health.fail_count }}</td> |
1253 | + <td>{{ overall_test_health.fail_percent|default_if_none:0|floatformat }}%</td> |
1254 | + <td><a |
1255 | + href="{% url dashboard_app.views.image_test_history image_health.rootfs_type image_health.hwpack_type test.test_id %}" |
1256 | + >{{ overall_test_health.total_run_count }}</a></td> |
1257 | + <td>{{ overall_test_health.total_count }}</td> |
1258 | + <td>{{ current_test_health.pass_count|default:0 }}</td> |
1259 | + <td>{{ current_test_health.fail_count|default:0 }}</td> |
1260 | + <td>{{ current_test_health.fail_percent|default_if_none:0|floatformat }}%</td> |
1261 | + <td><a |
1262 | + href="{{ current_test_health.test_run.get_absolute_url }}" |
1263 | + >{{ current_test_health.total_count|default:0 }}</a></td> |
1264 | + <td>{{ test.name|default:"<em>not set</em>" }}</td> |
1265 | + </tr> |
1266 | + {% endcall %} |
1267 | + {% endcall %} |
1268 | + {% endfor %} |
1269 | + </tbody> |
1270 | +</table> |
1271 | +{% endblock %} |
1272 | |
1273 | === added file 'dashboard_app/templates/dashboard_app/image_status_list.html' |
1274 | --- dashboard_app/templates/dashboard_app/image_status_list.html 1970-01-01 00:00:00 +0000 |
1275 | +++ dashboard_app/templates/dashboard_app/image_status_list.html 2011-07-22 01:34:34 +0000 |
1276 | @@ -0,0 +1,38 @@ |
1277 | +{% extends "dashboard_app/_content.html" %} |
1278 | +{% load call %} |
1279 | + |
1280 | + |
1281 | +{% block content %} |
1282 | +<script type="text/javascript" charset="utf-8"> |
1283 | + $(document).ready(function() { |
1284 | + oTable = $('#LEBs').dataTable({ |
1285 | + "bJQueryUI": true, |
1286 | + "bPaginate": false, |
1287 | + }); |
1288 | + }); |
1289 | +</script> |
1290 | +<table class="demo_jui display" id="LEBs"> |
1291 | + <thead> |
1292 | + <tr> |
1293 | + <th></th> |
1294 | + {% for hwpack in hwpack_list %} |
1295 | + <th>{{ hwpack }}</th> |
1296 | + {% endfor %} |
1297 | + </tr> |
1298 | + </thead> |
1299 | + <tbody> |
1300 | + {% for rootfs in rootfs_list %} |
1301 | + <tr> |
1302 | + <th>{{ rootfs }}</th> |
1303 | + {% for hwpack in hwpack_list %} |
1304 | + <td> |
1305 | + {% call ImageHealth rootfs hwpack as image_health %} |
1306 | + <a href="{{ image_health.get_absolute_url }}">{{ image_health.get_test_runs.count }} test runs</a> |
1307 | + {% endcall %} |
1308 | + </td> |
1309 | + {% endfor %} |
1310 | + </tr> |
1311 | + {% endfor %} |
1312 | + </tbody> |
1313 | +</table> |
1314 | +{% endblock %} |
1315 | |
1316 | === added file 'dashboard_app/templates/dashboard_app/image_test_history.html' |
1317 | --- dashboard_app/templates/dashboard_app/image_test_history.html 1970-01-01 00:00:00 +0000 |
1318 | +++ dashboard_app/templates/dashboard_app/image_test_history.html 2011-07-22 01:34:34 +0000 |
1319 | @@ -0,0 +1,15 @@ |
1320 | +{% extends "dashboard_app/_content.html" %} |
1321 | + |
1322 | + |
1323 | +{% block content %} |
1324 | +<script type="text/javascript"> |
1325 | + $(document).ready(function() { |
1326 | + $('#test_runs').dataTable({ |
1327 | + bJQueryUI: true, |
1328 | + sPaginationType: "full_numbers", |
1329 | + aaSorting: [[0, "desc"]], |
1330 | + }); |
1331 | + }); |
1332 | +</script> |
1333 | +{% include "dashboard_app/_test_run_list_table.html" %} |
1334 | +{% endblock %} |
1335 | |
1336 | === modified file 'dashboard_app/templates/dashboard_app/index.html' |
1337 | --- dashboard_app/templates/dashboard_app/index.html 2011-07-12 02:34:12 +0000 |
1338 | +++ dashboard_app/templates/dashboard_app/index.html 2011-07-22 01:34:34 +0000 |
1339 | @@ -1,10 +1,58 @@ |
1340 | {% extends "dashboard_app/_content.html" %} |
1341 | |
1342 | + |
1343 | {% block content %} |
1344 | -<h1>TODO</h1> |
1345 | -<ul> |
1346 | - <li>Briefly mention key dashboard features</li> |
1347 | - <li>Link to readthedocs dashboard manual</li> |
1348 | - <li>Add sensible dashboard index page (recent/interesting stuff)</li> |
1349 | +<h1>Welcome</h1> |
1350 | +<p>The <em>Validation Dashboard</em> is your window to |
1351 | +test results, regardless of how your run your tests you |
1352 | +can upload the results here and analyze them with simple |
1353 | +built-in reports as well as arbitrary custom reports and |
1354 | +data mining queries.</p> |
1355 | + |
1356 | +<h2>Key Features</h2> |
1357 | +<ul> |
1358 | + <li>Online repository of test results, with simple to use, web APIs and |
1359 | + command line tools for uploading test results.</li> |
1360 | + <li>Test results are packaged in documents (bundles) that you can easily sync |
1361 | + across systems, model is similar to the one used by git</li> |
1362 | + <li>Test results can refer to software and hardware context so that you know |
1363 | + exactly what software and hardware combination fails</li> |
1364 | + <li>Data mining and reporting allows users to create custom tailored reports |
1365 | + based on the data in the system</li> |
1366 | + <li>Distributed work-flow model, with some data privacy out of the box, fully |
1367 | + private installation can be deployed in minutes.</li> |
1368 | +</ul> |
1369 | + |
1370 | +<h2>Documentation & Get Started</h2> |
1371 | +<p>To get started quickly follow the link below, if you feel that an important |
1372 | +content is missing please <a |
1373 | + href="https://bugs.launchpad.net/lava-dashboard/+filebug" |
1374 | + >report a bug</a> or <a |
1375 | + href="https://answers.launchpad.net/lava-dashboard/+addquestion" |
1376 | + >ask a question</a>. Please make sure to report dashboard version (you are |
1377 | +currently using version {{lava.extensions.as_mapping.dashboard_app.version}})</p> |
1378 | +<p>All documentation is hosted on <a |
1379 | + href="http://readthedocs.org/docs/lava-dashboard/en/latest/">ReadTheDocs.org</a>.</p> |
1380 | + |
1381 | +<h3>Developers</h3> |
1382 | +<ul> |
1383 | + <li>How to put test results of my test suite into the Dashboard?</li> |
1384 | + <li>How to integrate my testing toolkit with the Dashboard?</li> |
1385 | + <li>How to allow users of my application to send anonymous qualitative and |
1386 | + quantitative (tests and benchmarks) data from their systems?</li> |
1387 | +</ul> |
1388 | + |
1389 | +<h3>Managers</h3> |
1390 | +<ul> |
1391 | + <li>What kind of reporting features are available out of the box?</li> |
1392 | + <li>How to create additional reports?</li> |
1393 | + <li>What kind of data is available in the system</li> |
1394 | +</ul> |
1395 | + |
1396 | +<h3>System Administrators</h3> |
1397 | +<ul> |
1398 | + <li>System requirements</li> |
1399 | + <li>How to deploy or upgrade the dashboard?</li> |
1400 | + <li>How to backup and restore the data</li> |
1401 | </ul> |
1402 | {% endblock %} |
1403 | |
1404 | === modified file 'dashboard_app/templates/dashboard_app/report_detail.html' |
1405 | --- dashboard_app/templates/dashboard_app/report_detail.html 2011-07-13 11:07:18 +0000 |
1406 | +++ dashboard_app/templates/dashboard_app/report_detail.html 2011-07-22 01:34:34 +0000 |
1407 | @@ -48,3 +48,12 @@ |
1408 | }); |
1409 | </script> |
1410 | {% endblock %} |
1411 | + |
1412 | +{% block body %} |
1413 | +{% if is_iframe %} |
1414 | +{{ report.get_html|safe}} |
1415 | +{% else %} |
1416 | +{{ block.super }} |
1417 | +{% endif %} |
1418 | +{% endblock %} |
1419 | + |
1420 | |
1421 | === modified file 'dashboard_app/templates/dashboard_app/test_result_detail.html' |
1422 | --- dashboard_app/templates/dashboard_app/test_result_detail.html 2011-07-12 02:34:12 +0000 |
1423 | +++ dashboard_app/templates/dashboard_app/test_result_detail.html 2011-07-22 01:34:34 +0000 |
1424 | @@ -4,15 +4,18 @@ |
1425 | |
1426 | |
1427 | {% block sidebar %} |
1428 | -<h3>{% trans "Hints" %}</h3> |
1429 | -<p class="hint"> |
1430 | -{% blocktrans %} |
1431 | -This is all the information that launch control has about this result. Log |
1432 | -analyzers can provide additional information by scrubbing it from the log file. |
1433 | -Information that is global to a test run can be attached to test run attributes |
1434 | -instead. |
1435 | -{% endblocktrans %} |
1436 | -</p> |
1437 | +<div class="ui-widget"> |
1438 | + <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0.7em"> |
1439 | + <span |
1440 | + class="ui-icon ui-icon-info" |
1441 | + style="float: left; margin-right: 0.3em;"></span> |
1442 | + <strong>Note:</strong> This is all the information that the dashboard has |
1443 | + about this result. Log analyzers can provide additional information by |
1444 | + scrubbing it from the log file. Information that is global to a test run |
1445 | + can be attached to test run attributes instead. |
1446 | + </div> |
1447 | +</div> |
1448 | +<br/> |
1449 | {% if test_result.test_run.test_results.count > 1 %} |
1450 | <h3>Other results</h3> |
1451 | <p class="hint">Results from the same test run are available here</p> |
1452 | @@ -42,11 +45,25 @@ |
1453 | |
1454 | |
1455 | {% block content %} |
1456 | -<h2>{% trans "Detailed information about test result" %}</h2> |
1457 | -<p>{% trans "Launch Control has the following information about this test result" %}</p> |
1458 | +<h2>{% trans "Test Result Details" %}</h2> |
1459 | <dl> |
1460 | <dt>{% trans "Result ID:" %}</dt> |
1461 | - <dd>{{ test_result }}</dd> |
1462 | + <dd> |
1463 | + {{ test_result }} |
1464 | + <div class="ui-widget" style="width: 30em"> |
1465 | + <div class="ui-state-highlight ui-corner-all" style="padding: 0.7em"> |
1466 | + <span |
1467 | + class="ui-icon ui-icon-info" |
1468 | + style="float: left; margin-right: 0.3em;"></span> |
1469 | + <strong>{% trans "Note:" %}</strong> |
1470 | + {% blocktrans %} |
1471 | + You can navigate to this test result, regardless of the bundle stream it is |
1472 | + located in, by using this |
1473 | + {% endblocktrans %} |
1474 | + <a href="{{ test_result.get_permalink }}" >{% trans "permalink" %}</a> |
1475 | + </div> |
1476 | + </div> |
1477 | + </dd> |
1478 | <dt>{% trans "Test case:" %}</dt> |
1479 | <dd> |
1480 | {% if test_result.test_case %} |
1481 | @@ -54,7 +71,7 @@ |
1482 | {% else %} |
1483 | <i>{% trans "unknown test case" %}</i> |
1484 | {% endif %} |
1485 | - {% trans "from test" %} <b>{{ test_result.test_run.test }}</b> |
1486 | + {% trans "from test" %} <b><a href="{{ test_result.test_run.test.get_absolute_url }}">{{ test_result.test_run.test }}</a></b> |
1487 | </dd> |
1488 | <dt>{% trans "Test outcome:" %}</dt> |
1489 | <dd>{{ test_result.get_result_display }}</dd> |
1490 | |
1491 | === modified file 'dashboard_app/templates/dashboard_app/test_run_detail.html' |
1492 | --- dashboard_app/templates/dashboard_app/test_run_detail.html 2011-07-13 17:28:46 +0000 |
1493 | +++ dashboard_app/templates/dashboard_app/test_run_detail.html 2011-07-22 01:34:34 +0000 |
1494 | @@ -43,9 +43,9 @@ |
1495 | {% block sidebar %} |
1496 | <dl> |
1497 | <dt>{% trans "Test Run UUID" %}</dt> |
1498 | - <dd>{{ test_run.analyzer_assigned_uuid }}</dd> |
1499 | + <dd>{{ test_run.analyzer_assigned_uuid }} <a href="{% url dashboard_app.views.redirect_to_test_run test_run.analyzer_assigned_uuid %}">{% trans "permalink" %}</a></dd> |
1500 | <dt>{% trans "Test Name" %}</dt> |
1501 | - <dd>{{ test_run.test }}</dd> |
1502 | + <dd><a href="{{ test_run.test.get_absolute_url }}">{{ test_run.test }}</a></dd> |
1503 | <dt>{% trans "OS Distribution" %}</dt> |
1504 | <dd>{{ test_run.sw_image_desc|default:"<i>Unspecified</i>" }}</dd> |
1505 | <dt>{% trans "Bundle SHA1" %}</dt> |
1506 | |
1507 | === added file 'dashboard_app/templatetags/call.py' |
1508 | --- dashboard_app/templatetags/call.py 1970-01-01 00:00:00 +0000 |
1509 | +++ dashboard_app/templatetags/call.py 2011-07-22 01:34:34 +0000 |
1510 | @@ -0,0 +1,64 @@ |
1511 | +from django import template |
1512 | + |
1513 | + |
1514 | +register = template.Library() |
1515 | + |
1516 | + |
1517 | +class CallNode(template.Node): |
1518 | + def __init__(self, func, args, name, nodelist): |
1519 | + self.func = func |
1520 | + self.args = args |
1521 | + self.name = name |
1522 | + self.nodelist = nodelist |
1523 | + |
1524 | + def __repr__(self): |
1525 | + return "<CallNode>" |
1526 | + |
1527 | + def _lookup_func(self, context): |
1528 | + parts = self.func.split('.') |
1529 | + current = context[parts[0]] |
1530 | + for part in parts[1:]: |
1531 | + if part.startswith("_"): |
1532 | + raise ValueError( |
1533 | + "Function cannot traverse private implementation attributes") |
1534 | + current = getattr(current, part) |
1535 | + return current |
1536 | + |
1537 | + def render(self, context): |
1538 | + try: |
1539 | + func = self._lookup_func(context) |
1540 | + values = [template.Variable(arg).resolve(context) for arg in self.args] |
1541 | + context.push() |
1542 | + context[self.name] = func(*values) |
1543 | + output = self.nodelist.render(context) |
1544 | + context.pop() |
1545 | + return output |
1546 | + except Exception as ex: |
1547 | + import logging |
1548 | + logging.exception("Unable to call %s with %r: %s", |
1549 | + self.func, self.args, ex) |
1550 | + raise |
1551 | + |
1552 | +def do_call(parser, token): |
1553 | + """ |
1554 | + Adds a value to the context (inside of this block) for caching and easy |
1555 | + access. |
1556 | + |
1557 | + For example:: |
1558 | + |
1559 | + {% call func 1 2 3 as result %} |
1560 | + {{ result }} |
1561 | + {% endcall %} |
1562 | + """ |
1563 | + bits = list(token.split_contents()) |
1564 | + if len(bits) < 2 or bits[-2] != "as": |
1565 | + raise template.TemplateSyntaxError( |
1566 | + "%r expected format is 'call func [args] [as name]'" % bits[0]) |
1567 | + func = bits[1] |
1568 | + args = bits[2:-2] |
1569 | + name = bits[-1] |
1570 | + nodelist = parser.parse(('endcall',)) |
1571 | + parser.delete_first_token() |
1572 | + return CallNode(func, args, name, nodelist) |
1573 | + |
1574 | +do_call = register.tag('call', do_call) |
1575 | |
1576 | === modified file 'dashboard_app/tests/__init__.py' |
1577 | --- dashboard_app/tests/__init__.py 2011-07-13 11:24:08 +0000 |
1578 | +++ dashboard_app/tests/__init__.py 2011-07-22 01:34:34 +0000 |
1579 | @@ -24,11 +24,11 @@ |
1580 | 'other.deserialization', |
1581 | 'other.login', |
1582 | 'other.test_client', |
1583 | - 'other.xml_rpc', |
1584 | 'regressions.LP658917', |
1585 | 'views.bundle_stream_list_view', |
1586 | 'views.test_run_detail_view', |
1587 | 'views.test_run_list_view', |
1588 | + 'views.redirects', |
1589 | ] |
1590 | |
1591 | def load_tests_from_submodules(_locals): |
1592 | |
1593 | === modified file 'dashboard_app/tests/other/dataview.py' |
1594 | --- dashboard_app/tests/other/dataview.py 2011-07-07 11:38:53 +0000 |
1595 | +++ dashboard_app/tests/other/dataview.py 2011-07-22 01:34:34 +0000 |
1596 | @@ -16,12 +16,10 @@ |
1597 | # You should have received a copy of the GNU Affero General Public License |
1598 | # along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
1599 | |
1600 | -import unittest |
1601 | - |
1602 | from mocker import Mocker, expect |
1603 | from testtools import TestCase |
1604 | |
1605 | -from dashboard_app.dataview import DataView |
1606 | +from dashboard_app.models import DataView |
1607 | |
1608 | |
1609 | class DataViewHandlerTests(TestCase): |
1610 | @@ -45,7 +43,7 @@ |
1611 | |
1612 | def setUp(self): |
1613 | super(DataViewHandlerTests, self).setUp() |
1614 | - self.dataview = DataView.load_from_xml(self.text) |
1615 | + self.dataview = DataView.repository.load_from_xml_string(self.text) |
1616 | |
1617 | def test_name_parsed_ok(self): |
1618 | self.assertEqual(self.dataview.name, "foo") |
1619 | @@ -97,7 +95,6 @@ |
1620 | Test for DataView.get_connection() |
1621 | """ |
1622 | # Mock connections['dataview'] to return special connection |
1623 | - from django.db.utils import ConnectionDoesNotExist |
1624 | mocker = Mocker() |
1625 | connections = mocker.replace("django.db.connections") |
1626 | special_connection = mocker.mock() |
1627 | |
1628 | === removed file 'dashboard_app/tests/other/xml_rpc.py' |
1629 | --- dashboard_app/tests/other/xml_rpc.py 2011-05-23 17:02:43 +0000 |
1630 | +++ dashboard_app/tests/other/xml_rpc.py 1970-01-01 00:00:00 +0000 |
1631 | @@ -1,132 +0,0 @@ |
1632 | -# Copyright (C) 2010 Linaro Limited |
1633 | -# |
1634 | -# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> |
1635 | -# |
1636 | -# This file is part of Launch Control. |
1637 | -# |
1638 | -# Launch Control is free software: you can redistribute it and/or modify |
1639 | -# it under the terms of the GNU Affero General Public License version 3 |
1640 | -# as published by the Free Software Foundation |
1641 | -# |
1642 | -# Launch Control is distributed in the hope that it will be useful, |
1643 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1644 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1645 | -# GNU General Public License for more details. |
1646 | -# |
1647 | -# You should have received a copy of the GNU Affero General Public License |
1648 | -# along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
1649 | - |
1650 | -""" |
1651 | -Unit tests of the Dashboard application |
1652 | -""" |
1653 | -import xmlrpclib |
1654 | - |
1655 | -from django_testscenarios.ubertest import TestCaseWithScenarios |
1656 | - |
1657 | -from dashboard_app.dispatcher import ( |
1658 | - DjangoXMLRPCDispatcher, |
1659 | - FaultCodes, |
1660 | - xml_rpc_signature, |
1661 | - ) |
1662 | - |
1663 | - |
1664 | -class TestAPI(object): |
1665 | - """ |
1666 | - Test API that gets exposed by the dispatcher for test runs. |
1667 | - """ |
1668 | - |
1669 | - @xml_rpc_signature() |
1670 | - def ping(self): |
1671 | - """ |
1672 | - Return "pong" message |
1673 | - """ |
1674 | - return "pong" |
1675 | - |
1676 | - def echo(self, arg): |
1677 | - """ |
1678 | - Return the argument back to the caller |
1679 | - """ |
1680 | - return arg |
1681 | - |
1682 | - def boom(self, code, string): |
1683 | - """ |
1684 | - Raise a Fault exception with the specified code and string |
1685 | - """ |
1686 | - raise xmlrpclib.Fault(code, string) |
1687 | - |
1688 | - def internal_boom(self): |
1689 | - """ |
1690 | - Raise a regular python exception (this should be hidden behind |
1691 | - an internal error fault) |
1692 | - """ |
1693 | - raise Exception("internal boom") |
1694 | - |
1695 | - |
1696 | -class DjangoXMLRPCDispatcherTestCase(TestCaseWithScenarios): |
1697 | - |
1698 | - def setUp(self): |
1699 | - super(DjangoXMLRPCDispatcherTestCase, self).setUp() |
1700 | - self.dispatcher = DjangoXMLRPCDispatcher() |
1701 | - self.dispatcher.register_instance(TestAPI()) |
1702 | - |
1703 | - def xml_rpc_call(self, method, *args): |
1704 | - """ |
1705 | - Perform XML-RPC call on our internal dispatcher instance |
1706 | - |
1707 | - This calls the method just like we would have normally from our view. |
1708 | - All arguments are marshaled and un-marshaled. XML-RPC fault exceptions |
1709 | - are raised like normal python exceptions (by xmlrpclib.loads) |
1710 | - """ |
1711 | - request = xmlrpclib.dumps(tuple(args), methodname=method) |
1712 | - response = self.dispatcher._marshaled_dispatch(request) |
1713 | - # This returns return value wrapped in a tuple and method name |
1714 | - # (which we don't have here as this is a response message). |
1715 | - return xmlrpclib.loads(response)[0][0] |
1716 | - |
1717 | - |
1718 | -class DjangoXMLRPCDispatcherTests(DjangoXMLRPCDispatcherTestCase): |
1719 | - |
1720 | - def test_standard_fault_code_for_missing_method(self): |
1721 | - try: |
1722 | - self.xml_rpc_call("method_that_hopefully_does_not_exist") |
1723 | - except xmlrpclib.Fault as ex: |
1724 | - self.assertEqual( |
1725 | - ex.faultCode, |
1726 | - FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND) |
1727 | - else: |
1728 | - self.fail("Calling missing method did not raise an exception") |
1729 | - |
1730 | - def test_ping(self): |
1731 | - retval = self.xml_rpc_call("ping") |
1732 | - self.assertEqual(retval, "pong") |
1733 | - |
1734 | - def test_echo(self): |
1735 | - self.assertEqual(self.xml_rpc_call("echo", 1), 1) |
1736 | - self.assertEqual(self.xml_rpc_call("echo", "string"), "string") |
1737 | - self.assertEqual(self.xml_rpc_call("echo", 1.5), 1.5) |
1738 | - |
1739 | - def test_boom(self): |
1740 | - self.assertRaises(xmlrpclib.Fault, |
1741 | - self.xml_rpc_call, "boom", 1, "str") |
1742 | - |
1743 | - |
1744 | -class DjangoXMLRPCDispatcherFaultCodeTests(DjangoXMLRPCDispatcherTestCase): |
1745 | - |
1746 | - scenarios = [ |
1747 | - ('method_not_found', { |
1748 | - 'method': "method_that_hopefully_does_not_exist", |
1749 | - 'faultCode': FaultCodes.ServerError.REQUESTED_METHOD_NOT_FOUND, |
1750 | - }), |
1751 | - ('internal_error', { |
1752 | - 'method': "internal_boom", |
1753 | - 'faultCode': FaultCodes.ServerError.INTERNAL_XML_RPC_ERROR, |
1754 | - }), |
1755 | - ] |
1756 | - |
1757 | - def test_standard_fault_codes(self): |
1758 | - try: |
1759 | - self.xml_rpc_call(self.method) |
1760 | - except xmlrpclib.Fault as ex: |
1761 | - self.assertEqual(ex.faultCode, self.faultCode) |
1762 | - else: |
1763 | - self.fail("Exception not raised") |
1764 | |
1765 | === modified file 'dashboard_app/tests/utils.py' |
1766 | --- dashboard_app/tests/utils.py 2011-05-30 16:53:27 +0000 |
1767 | +++ dashboard_app/tests/utils.py 2011-07-22 01:34:34 +0000 |
1768 | @@ -1,7 +1,6 @@ |
1769 | """ |
1770 | Django-specific test utilities |
1771 | """ |
1772 | -import os |
1773 | import xmlrpclib |
1774 | |
1775 | from django.conf import settings |
1776 | |
1777 | === added file 'dashboard_app/tests/views/redirects.py' |
1778 | --- dashboard_app/tests/views/redirects.py 1970-01-01 00:00:00 +0000 |
1779 | +++ dashboard_app/tests/views/redirects.py 2011-07-22 01:34:34 +0000 |
1780 | @@ -0,0 +1,75 @@ |
1781 | +# Copyright (C) 2010 Linaro Limited |
1782 | +# |
1783 | +# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org> |
1784 | +# |
1785 | +# This file is part of Launch Control. |
1786 | +# |
1787 | +# Launch Control is free software: you can redistribute it and/or modify |
1788 | +# it under the terms of the GNU Affero General Public License version 3 |
1789 | +# as published by the Free Software Foundation |
1790 | +# |
1791 | +# Launch Control is distributed in the hope that it will be useful, |
1792 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1793 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1794 | +# GNU General Public License for more details. |
1795 | +# |
1796 | +# You should have received a copy of the GNU Affero General Public License |
1797 | +# along with Launch Control. If not, see <http://www.gnu.org/licenses/>. |
1798 | + |
1799 | +from django.core.urlresolvers import reverse |
1800 | +from django_testscenarios.ubertest import TestCase |
1801 | + |
1802 | +from dashboard_app.tests import fixtures |
1803 | + |
1804 | + |
1805 | +class RedirectTests(TestCase): |
1806 | + |
1807 | + _PATHNAME = "/anonymous/" |
1808 | + _BUNDLE_TEXT = """ |
1809 | +{ |
1810 | + "test_runs": [ |
1811 | + { |
1812 | + "test_results": [ |
1813 | + { |
1814 | + "test_case_id": "test-case-0", |
1815 | + "result": "pass" |
1816 | + } |
1817 | + ], |
1818 | + "analyzer_assigned_date": "2010-10-15T22:04:46Z", |
1819 | + "time_check_performed": false, |
1820 | + "analyzer_assigned_uuid": "00000000-0000-0000-0000-000000000001", |
1821 | + "test_id": "examples" |
1822 | + } |
1823 | + ], |
1824 | + "format": "Dashboard Bundle Format 1.0" |
1825 | +} |
1826 | + """ |
1827 | + _BUNDLE_NAME = "whatever.json" |
1828 | + |
1829 | + def setUp(self): |
1830 | + super(RedirectTests, self).setUp() |
1831 | + self.bundle = fixtures.create_bundle(self._PATHNAME, self._BUNDLE_TEXT, self._BUNDLE_NAME) |
1832 | + self.bundle.deserialize() |
1833 | + self.assertTrue(self.bundle.is_deserialized) |
1834 | + |
1835 | + def test_bundle_permalink(self): |
1836 | + response = self.client.get( |
1837 | + reverse("dashboard_app.views.redirect_to_bundle", |
1838 | + args=(self.bundle.content_sha1, ))) |
1839 | + self.assertRedirects(response, self.bundle.get_absolute_url()) |
1840 | + |
1841 | + def test_test_run_permalink(self): |
1842 | + test_run = self.bundle.test_runs.all()[0] |
1843 | + response = self.client.get( |
1844 | + reverse("dashboard_app.views.redirect_to_test_run", |
1845 | + args=(test_run.analyzer_assigned_uuid, ))) |
1846 | + self.assertRedirects(response, test_run.get_absolute_url()) |
1847 | + |
1848 | + def test_test_result_permalink(self): |
1849 | + test_run = self.bundle.test_runs.all()[0] |
1850 | + test_result = test_run.test_results.all()[0] |
1851 | + response = self.client.get( |
1852 | + reverse("dashboard_app.views.redirect_to_test_result", |
1853 | + args=(test_run.analyzer_assigned_uuid, |
1854 | + test_result.relative_index))) |
1855 | + self.assertRedirects(response, test_result.get_absolute_url()) |
1856 | |
1857 | === modified file 'dashboard_app/tests/views/test_run_detail_view.py' |
1858 | --- dashboard_app/tests/views/test_run_detail_view.py 2011-07-12 02:33:19 +0000 |
1859 | +++ dashboard_app/tests/views/test_run_detail_view.py 2011-07-22 01:34:34 +0000 |
1860 | @@ -20,7 +20,6 @@ |
1861 | from django_testscenarios.ubertest import (TestCase, TestCaseWithScenarios) |
1862 | from dashboard_app.models import BundleStream, TestRun |
1863 | from django.contrib.auth.models import (User, Group) |
1864 | -from django.core.urlresolvers import reverse |
1865 | |
1866 | from dashboard_app.tests.utils import TestClient |
1867 | |
1868 | |
1869 | === modified file 'dashboard_app/urls.py' |
1870 | --- dashboard_app/urls.py 2011-07-13 12:29:53 +0000 |
1871 | +++ dashboard_app/urls.py 2011-07-22 01:34:34 +0000 |
1872 | @@ -55,4 +55,10 @@ |
1873 | 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'), |
1874 | 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'), |
1875 | url(r'^streams(?P<pathname>/[a-zA-Z0-9/_-]+)test-runs/$', 'test_run_list'), |
1876 | + url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'redirect_to_test_run'), |
1877 | + url(r'^permalink/test-result/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<relative_index>[0-9]+)/$', 'redirect_to_test_result'), |
1878 | + url(r'^permalink/bundle/(?P<content_sha1>[0-9a-z]+)/$', 'redirect_to_bundle'), |
1879 | + url(r'^image_status/$', 'image_status_list'), |
1880 | + url(r'^image_status/(?P<rootfs_type>[a-zA-Z0-9_-]+)\+(?P<hwpack_type>[a-zA-Z0-9_-]+)/$', 'image_status_detail'), |
1881 | + url(r'^image_status/(?P<rootfs_type>[a-zA-Z0-9_-]+)\+(?P<hwpack_type>[a-zA-Z0-9_-]+)/test-history/(?P<test_id>[^/]+)/$', 'image_test_history'), |
1882 | ) |
1883 | |
1884 | === modified file 'dashboard_app/views.py' |
1885 | --- dashboard_app/views.py 2011-07-13 17:28:57 +0000 |
1886 | +++ dashboard_app/views.py 2011-07-22 01:34:34 +0000 |
1887 | @@ -20,25 +20,21 @@ |
1888 | Views for the Dashboard application |
1889 | """ |
1890 | |
1891 | -from django.contrib.auth.decorators import login_required |
1892 | -from django.contrib.csrf.middleware import csrf_exempt |
1893 | -from django.contrib.sites.models import Site |
1894 | from django.db.models.manager import Manager |
1895 | from django.db.models.query import QuerySet |
1896 | -from django.http import (HttpResponse, Http404) |
1897 | -from django.shortcuts import render_to_response |
1898 | +from django.http import Http404 |
1899 | +from django.shortcuts import render_to_response, redirect, get_object_or_404 |
1900 | from django.template import RequestContext |
1901 | from django.views.generic.list_detail import object_list, object_detail |
1902 | |
1903 | -from dashboard_app.dataview import DataView, DataViewRepository |
1904 | -from dashboard_app.dispatcher import DjangoXMLRPCDispatcher |
1905 | from dashboard_app.models import ( |
1906 | Attachment, |
1907 | Bundle, |
1908 | BundleStream, |
1909 | DataReport, |
1910 | + DataView, |
1911 | + ImageHealth, |
1912 | Test, |
1913 | - TestCase, |
1914 | TestResult, |
1915 | TestRun, |
1916 | ) |
1917 | @@ -276,7 +272,7 @@ |
1918 | |
1919 | |
1920 | @BreadCrumb( |
1921 | - "Result {relative_index}", |
1922 | + "Details of result {relative_index}", |
1923 | parent=test_run_detail, |
1924 | needs=['pathname', 'content_sha1', 'analyzer_assigned_uuid', 'relative_index']) |
1925 | def test_result_detail(request, pathname, content_sha1, analyzer_assigned_uuid, relative_index): |
1926 | @@ -384,31 +380,39 @@ |
1927 | raise Http404('No report matches given name.') |
1928 | return render_to_response( |
1929 | "dashboard_app/report_detail.html", { |
1930 | - 'bread_crumb_trail': BreadCrumbTrail.leading_to(report_detail, name=report.name, title=report.title), |
1931 | + "is_iframe": request.GET.get("iframe") == "yes", |
1932 | + 'bread_crumb_trail': BreadCrumbTrail.leading_to( |
1933 | + report_detail, |
1934 | + name=report.name, |
1935 | + title=report.title), |
1936 | "report": report, |
1937 | }, RequestContext(request)) |
1938 | |
1939 | |
1940 | @BreadCrumb("Data views", parent=index) |
1941 | def data_view_list(request): |
1942 | - repo = DataViewRepository.get_instance() |
1943 | return render_to_response( |
1944 | "dashboard_app/data_view_list.html", { |
1945 | 'bread_crumb_trail': BreadCrumbTrail.leading_to(data_view_list), |
1946 | - "data_view_list": repo.data_views |
1947 | + "data_view_list": DataView.repository.all(), |
1948 | }, RequestContext(request)) |
1949 | |
1950 | |
1951 | -@BreadCrumb("Details of {name}", parent=data_view_list, needs=['name']) |
1952 | +@BreadCrumb( |
1953 | + "Details of {name}", |
1954 | + parent=data_view_list, |
1955 | + needs=['name']) |
1956 | def data_view_detail(request, name): |
1957 | - repo = DataViewRepository.get_instance() |
1958 | try: |
1959 | - data_view = repo[name] |
1960 | - except KeyError: |
1961 | + data_view = DataView.repository.get(name=name) |
1962 | + except DataView.DoesNotExist: |
1963 | raise Http404('No data view matches the given query.') |
1964 | return render_to_response( |
1965 | "dashboard_app/data_view_detail.html", { |
1966 | - 'bread_crumb_trail': BreadCrumbTrail.leading_to(data_view_detail, name=data_view.name, summary=data_view.summary), |
1967 | + 'bread_crumb_trail': BreadCrumbTrail.leading_to( |
1968 | + data_view_detail, |
1969 | + name=data_view.name, |
1970 | + summary=data_view.summary), |
1971 | "data_view": data_view |
1972 | }, RequestContext(request)) |
1973 | |
1974 | @@ -437,3 +441,80 @@ |
1975 | extra_context={ |
1976 | 'bread_crumb_trail': BreadCrumbTrail.leading_to(test_detail, test_id=test_id) |
1977 | }) |
1978 | + |
1979 | + |
1980 | +def redirect_to_test_run(request, analyzer_assigned_uuid): |
1981 | + test_run = get_restricted_object_or_404( |
1982 | + TestRun, |
1983 | + lambda test_run: test_run.bundle.bundle_stream, |
1984 | + request.user, |
1985 | + analyzer_assigned_uuid=analyzer_assigned_uuid) |
1986 | + return redirect(test_run.get_absolute_url()) |
1987 | + |
1988 | + |
1989 | +def redirect_to_test_result(request, analyzer_assigned_uuid, relative_index): |
1990 | + test_result = get_restricted_object_or_404( |
1991 | + TestResult, |
1992 | + lambda test_result: test_result.test_run.bundle.bundle_stream, |
1993 | + request.user, |
1994 | + test_run__analyzer_assigned_uuid=analyzer_assigned_uuid, |
1995 | + relative_index=relative_index) |
1996 | + return redirect(test_result.get_absolute_url()) |
1997 | + |
1998 | + |
1999 | +def redirect_to_bundle(request, content_sha1): |
2000 | + bundle = get_restricted_object_or_404( |
2001 | + Bundle, |
2002 | + lambda bundle: bundle.bundle_stream, |
2003 | + request.user, |
2004 | + content_sha1=content_sha1) |
2005 | + return redirect(bundle.get_absolute_url()) |
2006 | + |
2007 | + |
2008 | +@BreadCrumb("Image Status Matrix", parent=index) |
2009 | +def image_status_list(request): |
2010 | + return render_to_response( |
2011 | + "dashboard_app/image_status_list.html", { |
2012 | + 'hwpack_list': ImageHealth.get_hwpack_list(), |
2013 | + 'rootfs_list': ImageHealth.get_rootfs_list(), |
2014 | + 'ImageHealth': ImageHealth, |
2015 | + 'bread_crumb_trail': BreadCrumbTrail.leading_to(image_status_list) |
2016 | + }, RequestContext(request)) |
2017 | + |
2018 | + |
2019 | +@BreadCrumb( |
2020 | + "Image Status for {rootfs_type} + {hwpack_type}", |
2021 | + parent=image_status_list, |
2022 | + needs=["rootfs_type", "hwpack_type"]) |
2023 | +def image_status_detail(request, rootfs_type, hwpack_type): |
2024 | + image_health = ImageHealth(rootfs_type, hwpack_type) |
2025 | + return render_to_response( |
2026 | + "dashboard_app/image_status_detail.html", { |
2027 | + 'image_health': image_health, |
2028 | + 'bread_crumb_trail': BreadCrumbTrail.leading_to( |
2029 | + image_status_detail, |
2030 | + rootfs_type=rootfs_type, |
2031 | + hwpack_type=hwpack_type), |
2032 | + }, RequestContext(request)) |
2033 | + |
2034 | + |
2035 | +@BreadCrumb( |
2036 | + "Test history for {test_id}", |
2037 | + parent=image_status_detail, |
2038 | + needs=["rootfs_type", "hwpack_type", "test_id"]) |
2039 | +def image_test_history(request, rootfs_type, hwpack_type, test_id): |
2040 | + image_health = ImageHealth(rootfs_type, hwpack_type) |
2041 | + test = get_object_or_404(Test, test_id=test_id) |
2042 | + test_run_list = image_health.get_test_runs().filter(test=test) |
2043 | + return render_to_response( |
2044 | + "dashboard_app/image_test_history.html", { |
2045 | + 'test_run_list': test_run_list, |
2046 | + 'test': test, |
2047 | + 'image_health': image_health, |
2048 | + 'bread_crumb_trail': BreadCrumbTrail.leading_to( |
2049 | + image_test_history, |
2050 | + rootfs_type=rootfs_type, |
2051 | + hwpack_type=hwpack_type, |
2052 | + test=test, |
2053 | + test_id=test_id), |
2054 | + }, RequestContext(request)) |
2055 | |
2056 | === modified file 'dashboard_app/xmlrpc.py' |
2057 | --- dashboard_app/xmlrpc.py 2011-07-07 11:39:05 +0000 |
2058 | +++ dashboard_app/xmlrpc.py 2011-07-22 01:34:34 +0000 |
2059 | @@ -24,15 +24,20 @@ |
2060 | import logging |
2061 | import xmlrpclib |
2062 | |
2063 | -from django.contrib.auth.models import User |
2064 | +from django.contrib.auth.models import User, Group |
2065 | from django.db import IntegrityError, DatabaseError |
2066 | -from linaro_django_xmlrpc.models import ExposedAPI |
2067 | -from linaro_django_xmlrpc.models import Mapper |
2068 | +from linaro_django_xmlrpc.models import ( |
2069 | + ExposedAPI, |
2070 | + Mapper, |
2071 | + xml_rpc_signature, |
2072 | +) |
2073 | |
2074 | from dashboard_app import __version__ |
2075 | -from dashboard_app.dataview import DataView, DataViewRepository |
2076 | -from dashboard_app.dispatcher import xml_rpc_signature |
2077 | -from dashboard_app.models import Bundle, BundleStream |
2078 | +from dashboard_app.models import ( |
2079 | + Bundle, |
2080 | + BundleStream, |
2081 | + DataView, |
2082 | +) |
2083 | |
2084 | |
2085 | class errors: |
2086 | @@ -57,10 +62,8 @@ |
2087 | All public methods are automatically exposed as XML-RPC methods |
2088 | """ |
2089 | |
2090 | - |
2091 | data_view_connection = DataView.get_connection() |
2092 | |
2093 | - |
2094 | @xml_rpc_signature('str') |
2095 | def version(self): |
2096 | """ |
2097 | @@ -137,8 +140,8 @@ |
2098 | ------------------------------ |
2099 | The following rules govern bundle stream upload access rights: |
2100 | - all anonymous streams are accessible |
2101 | - - personal streams are accessible by owners |
2102 | - - team streams are accessible by team members |
2103 | + - personal streams are accessible to owners |
2104 | + - team streams are accessible to team members |
2105 | |
2106 | """ |
2107 | try: |
2108 | @@ -201,8 +204,8 @@ |
2109 | ------------------------------ |
2110 | The following rules govern bundle stream download access rights: |
2111 | - all anonymous streams are accessible |
2112 | - - personal streams are accessible by owners |
2113 | - - team streams are accessible by team members |
2114 | + - personal streams are accessible to owners |
2115 | + - team streams are accessible to team members |
2116 | """ |
2117 | try: |
2118 | bundle = Bundle.objects.get(content_sha1=content_sha1) |
2119 | @@ -256,8 +259,8 @@ |
2120 | ------------------------------ |
2121 | The following rules govern bundle stream download access rights: |
2122 | - all anonymous streams are accessible |
2123 | - - personal streams are accessible by owners |
2124 | - - team streams are accessible by team members |
2125 | + - personal streams are accessible to owners |
2126 | + - team streams are accessible to team members |
2127 | """ |
2128 | bundle_streams = BundleStream.objects.accessible_by_principal(self.user) |
2129 | return [{ |
2130 | @@ -319,8 +322,8 @@ |
2131 | ------------------------------ |
2132 | The following rules govern bundle stream download access rights: |
2133 | - all anonymous streams are accessible |
2134 | - - personal streams are accessible by owners |
2135 | - - team streams are accessible by team members |
2136 | + - personal streams are accessible to owners |
2137 | + - team streams are accessible to team members |
2138 | """ |
2139 | try: |
2140 | bundle_stream = BundleStream.objects.accessible_by_principal(self.user).get(pathname=pathname) |
2141 | @@ -418,20 +421,55 @@ |
2142 | if name is None: |
2143 | name = "" |
2144 | try: |
2145 | - user, group, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname) |
2146 | + user_name, group_name, slug, is_public, is_anonymous = BundleStream.parse_pathname(pathname) |
2147 | except ValueError as ex: |
2148 | raise xmlrpclib.Fault(errors.FORBIDDEN, str(ex)) |
2149 | - if user is None and group is None: |
2150 | + |
2151 | + # Start with those to simplify the logic below |
2152 | + user = None |
2153 | + group = None |
2154 | + if is_anonymous is False: |
2155 | + if self.user is not None: |
2156 | + assert is_anonymous is False |
2157 | + assert self.user is not None |
2158 | + if user_name is not None: |
2159 | + if user_name != self.user.username: |
2160 | + raise xmlrpclib.Fault( |
2161 | + errors.FORBIDDEN, |
2162 | + "Only user {user!r} could create this stream".format(user=user_name)) |
2163 | + user = self.user # map to real user object |
2164 | + elif group_name is not None: |
2165 | + try: |
2166 | + group = self.user.groups.get(name=group_name) |
2167 | + except Group.DoesNotExist: |
2168 | + raise xmlrpclib.Fault( |
2169 | + errors.FORBIDDEN, |
2170 | + "Only a member of group {group!r} could create this stream".format(group=group_name)) |
2171 | + else: |
2172 | + assert is_anonymous is False |
2173 | + assert self.user is None |
2174 | + raise xmlrpclib.Fault( |
2175 | + errors.FORBIDDEN, "Only anonymous streams can be constructed (you are not signed in)") |
2176 | + else: |
2177 | + assert is_anonymous is True |
2178 | + assert user_name is None |
2179 | + assert group_name is None |
2180 | # Hacky but will suffice for now |
2181 | user = User.objects.get_or_create(username="anonymous-owner")[0] |
2182 | - try: |
2183 | - bundle_stream = BundleStream.objects.create(user=user, group=group, slug=slug, is_public=is_public, is_anonymous=is_anonymous, name=name) |
2184 | - except IntegrityError: |
2185 | - raise xmlrpclib.Fault(errors.CONFLICT, "Stream with the specified pathname already exists") |
2186 | + try: |
2187 | + bundle_stream = BundleStream.objects.create( |
2188 | + user=user, |
2189 | + group=group, |
2190 | + slug=slug, |
2191 | + is_public=is_public, |
2192 | + is_anonymous=is_anonymous, |
2193 | + name=name) |
2194 | + except IntegrityError: |
2195 | + raise xmlrpclib.Fault( |
2196 | + errors.CONFLICT, |
2197 | + "Stream with the specified pathname already exists") |
2198 | else: |
2199 | - # TODO: Make this constraint unnecessary |
2200 | - raise xmlrpclib.Fault(errors.FORBIDDEN, "Only anonymous streams can be constructed") |
2201 | - return bundle_stream.pathname |
2202 | + return bundle_stream.pathname |
2203 | |
2204 | def data_views(self): |
2205 | """ |
2206 | @@ -461,7 +499,6 @@ |
2207 | ----------------- |
2208 | None |
2209 | """ |
2210 | - repo = DataViewRepository.get_instance() |
2211 | return [{ |
2212 | 'name': data_view.name, |
2213 | 'summary': data_view.summary, |
2214 | @@ -472,7 +509,7 @@ |
2215 | "help": arg.help, |
2216 | "default": arg.default |
2217 | } for arg in data_view.arguments] |
2218 | - } for data_view in repo] |
2219 | + } for data_view in DataView.repository.all()] |
2220 | |
2221 | def data_view_info(self, name): |
2222 | """ |
2223 | @@ -516,10 +553,9 @@ |
2224 | 404 |
2225 | Name does not designate a data view |
2226 | """ |
2227 | - repo = DataViewRepository.get_instance() |
2228 | try: |
2229 | - data_view = repo[name] |
2230 | - except KeyError: |
2231 | + data_view = DataView.repository.get(name=name) |
2232 | + except DataView.DoesNotExist: |
2233 | raise xmlrpclib.Fault(errors.NOT_FOUND, "Data view not found") |
2234 | else: |
2235 | query = data_view.get_backend_specific_query(self.data_view_connection) |
2236 | @@ -564,10 +600,9 @@ |
2237 | ----------------- |
2238 | TBD |
2239 | """ |
2240 | - repo = DataViewRepository.get_instance() |
2241 | try: |
2242 | - data_view = repo[name] |
2243 | - except KeyError: |
2244 | + data_view = DataView.repository.get(name=name) |
2245 | + except DataView.DoesNotExist: |
2246 | raise xmlrpclib.Fault(errors.NOT_FOUND, "Data view not found") |
2247 | try: |
2248 | rows, columns = data_view(self.data_view_connection, **arguments) |
2249 | @@ -584,7 +619,6 @@ |
2250 | } |
2251 | |
2252 | |
2253 | - |
2254 | # Mapper used by the legacy URL |
2255 | legacy_mapper = Mapper() |
2256 | legacy_mapper.register_introspection_methods() |
2257 | |
2258 | === modified file 'doc/changes.rst' |
2259 | --- doc/changes.rst 2011-06-29 22:11:18 +0000 |
2260 | +++ doc/changes.rst 2011-07-22 01:34:34 +0000 |
2261 | @@ -1,6 +1,45 @@ |
2262 | Version History |
2263 | *************** |
2264 | |
2265 | +.. _version_0_6: |
2266 | + |
2267 | +Version 0.6 |
2268 | +=========== |
2269 | + |
2270 | +This version was released as 2011.07 in the Linaro monthly release process. |
2271 | + |
2272 | +Release highlights: |
2273 | + |
2274 | +* New UI synchronized with lava-server, the UI is going to be changed in the |
2275 | + next release to be more in line with the official Linaro theme. Currently |
2276 | + most changes are under-the-hood, sporting more jQuery UI CSS. |
2277 | +* New test browser that allows to see all the registered tests and their test |
2278 | + cases. |
2279 | +* New data view browser, similar to data view browser. |
2280 | +* New permalink system that allows easy linking to bundles, test runs and test results. |
2281 | +* New image status views that allow for quick inspection of interesting |
2282 | + hardware pack + root filesystem combinations. |
2283 | +* New image status detail view with color-coded information about test failures |
2284 | + affecting current and historic instances of a particular root filesystem + |
2285 | + hardware pack combination. |
2286 | +* New image test history view showing all the runs of a particular test on a |
2287 | + particular combination of root filesystem + hardware pack. |
2288 | +* New table widget for better table display with support for client side |
2289 | + sorting and searching. |
2290 | +* New option to render data reports without any navigation that is suitable for |
2291 | + embedding inside an iframe (by appending &iframe=yes to the URL) |
2292 | +* New view for showing text attachments associated with test runs. |
2293 | +* New view showing test runs associated with a specific bundle. |
2294 | +* New view showing the raw JSON text of a bundle. |
2295 | +* New view for inspecting bundle deserialization failures. |
2296 | +* Integration with lava-server/RPC2/ for web APIs |
2297 | +* Added support for non-anonymous submissions (test results uploaded by |
2298 | + authenticated users), including uploading results to personal (owned by |
2299 | + person), team (owned by group), public (visible) and private (hidden from |
2300 | + non-owners) bundle streams. |
2301 | +* Added support for creating non-anonymous bundle streams with |
2302 | + dashboard.make_stream() (for authenticated users) |
2303 | + |
2304 | .. _version_0_5: |
2305 | |
2306 | Version 0.5 |
2307 | |
2308 | === modified file 'doc/index.rst' |
2309 | --- doc/index.rst 2011-06-29 22:11:18 +0000 |
2310 | +++ doc/index.rst 2011-07-22 01:34:34 +0000 |
2311 | @@ -5,7 +5,7 @@ |
2312 | .. automodule:: dashboard_app |
2313 | |
2314 | .. seealso:: To get started quickly see :ref:`usage` |
2315 | -.. seealso:: See what's new in :ref:`version_0_5` |
2316 | +.. seealso:: See what's new in :ref:`version_0_6` |
2317 | |
2318 | Features |
2319 | ======== |
Added a few more patches on top:
1) New LEB health pages
2) Minor feature that allows embedding reports in iframes
3) Permalinks for bundles, test runs and test results