Merge lp:~linaro-validation/lava-dashboard/0.6-wip into lp:lava-dashboard

Proposed by Zygmunt Krynicki
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
Reviewer Review Type Date Requested Status
Linaro Validation Team Pending
Review via email: mp+68142@code.launchpad.net

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.

To post a comment you must log in.
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

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

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

Revision history for this message
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

Revision history for this message
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 BaseContentHandler.characters()

268. By Zygmunt Krynicki

Clean up _DataReportHandler

269. By Zygmunt Krynicki

Clean up _DataViewHandler and DataViewRepostiory

This code mainly goes to match the DataReportRepository

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 &amp; 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 ========

Subscribers

People subscribed via source and target branches