Merge lp:~stevanr/lava-dashboard/image-reports-gui into lp:lava-dashboard

Proposed by Stevan Radaković
Status: Rejected
Rejected by: Stevan Radaković
Proposed branch: lp:~stevanr/lava-dashboard/image-reports-gui
Merge into: lp:lava-dashboard
Diff against target: 3914 lines (+3813/-0) (has conflicts)
17 files modified
dashboard_app/migrations/0032_auto__add_unique_imagereportchart_image_report_name.py (+306/-0)
dashboard_app/models.py.OTHER (+2089/-0)
dashboard_app/static/dashboard_app/css/image-charts.css.OTHER (+138/-0)
dashboard_app/static/dashboard_app/js/excanvas.min.js.OTHER (+1/-0)
dashboard_app/static/dashboard_app/js/image-chart.js (+291/-0)
dashboard_app/static/dashboard_app/js/jquery.flot.canvas.min.js (+28/-0)
dashboard_app/static/dashboard_app/js/jquery.flot.min.js.OTHER (+29/-0)
dashboard_app/static/dashboard_app/js/jquery.flot.navigate.min.js.OTHER (+86/-0)
dashboard_app/static/dashboard_app/js/jquery.flot.selection.min.js.OTHER (+79/-0)
dashboard_app/static/dashboard_app/js/jquery.flot.stack.min.js.OTHER (+36/-0)
dashboard_app/templates/dashboard_app/image_report_chart_detail.html.OTHER (+89/-0)
dashboard_app/templates/dashboard_app/image_report_chart_form.html.OTHER (+66/-0)
dashboard_app/templates/dashboard_app/image_report_detail.html.OTHER (+77/-0)
dashboard_app/templates/dashboard_app/image_report_display.html (+26/-0)
dashboard_app/templates/dashboard_app/image_report_list.html.OTHER (+40/-0)
dashboard_app/urls.py.OTHER (+91/-0)
dashboard_app/views/image_reports/views.py.OTHER (+341/-0)
Conflict adding files to dashboard_app.  Created directory.
Conflict because dashboard_app is not versioned, but has versioned children.  Versioned directory.
Conflict adding files to dashboard_app/migrations.  Created directory.
Conflict because dashboard_app/migrations is not versioned, but has versioned children.  Versioned directory.
Contents conflict in dashboard_app/models.py
Conflict adding files to dashboard_app/static.  Created directory.
Conflict because dashboard_app/static is not versioned, but has versioned children.  Versioned directory.
Conflict adding files to dashboard_app/static/dashboard_app.  Created directory.
Conflict because dashboard_app/static/dashboard_app is not versioned, but has versioned children.  Versioned directory.
Conflict adding files to dashboard_app/static/dashboard_app/css.  Created directory.
Conflict because dashboard_app/static/dashboard_app/css is not versioned, but has versioned children.  Versioned directory.
Contents conflict in dashboard_app/static/dashboard_app/css/image-charts.css
Conflict adding files to dashboard_app/static/dashboard_app/js.  Created directory.
Conflict because dashboard_app/static/dashboard_app/js is not versioned, but has versioned children.  Versioned directory.
Contents conflict in dashboard_app/static/dashboard_app/js/excanvas.min.js
Contents conflict in dashboard_app/static/dashboard_app/js/jquery.flot.min.js
Contents conflict in dashboard_app/static/dashboard_app/js/jquery.flot.navigate.min.js
Contents conflict in dashboard_app/static/dashboard_app/js/jquery.flot.selection.min.js
Contents conflict in dashboard_app/static/dashboard_app/js/jquery.flot.stack.min.js
Conflict adding files to dashboard_app/templates.  Created directory.
Conflict because dashboard_app/templates is not versioned, but has versioned children.  Versioned directory.
Conflict adding files to dashboard_app/templates/dashboard_app.  Created directory.
Conflict because dashboard_app/templates/dashboard_app is not versioned, but has versioned children.  Versioned directory.
Contents conflict in dashboard_app/templates/dashboard_app/image_report_chart_detail.html
Contents conflict in dashboard_app/templates/dashboard_app/image_report_chart_form.html
Contents conflict in dashboard_app/templates/dashboard_app/image_report_detail.html
Contents conflict in dashboard_app/templates/dashboard_app/image_report_list.html
Contents conflict in dashboard_app/urls.py
Conflict adding files to dashboard_app/views.  Created directory.
Conflict because dashboard_app/views is not versioned, but has versioned children.  Versioned directory.
Conflict adding files to dashboard_app/views/image_reports.  Created directory.
Conflict because dashboard_app/views/image_reports is not versioned, but has versioned children.  Versioned directory.
Contents conflict in dashboard_app/views/image_reports/views.py
To merge this branch: bzr merge lp:~stevanr/lava-dashboard/image-reports-gui
Reviewer Review Type Date Requested Status
Stevan Radaković Disapprove
Review via email: mp+187003@code.launchpad.net

Description of the change

Test MP. Do not merge.

To post a comment you must log in.
Revision history for this message
Stevan Radaković (stevanr) :
review: Disapprove

Unmerged revisions

432. By Stevan Radaković

Add filters links below headline.

431. By Stevan Radaković

Add dates, introduce canvas plugin.

430. By Stevan Radaković

Update jquery flot library and plugins. Add canvas plugin.

429. By Stevan Radaković

Fix image rendering.

428. By Stevan Radaković

Add hover event to the plot.

427. By Stevan Radaković

Multiple small changes. Interactibility added.

426. By Stevan Radaković

Display for reports with charts and legends with sortable plugin.

425. By Stevan Radaković

Add simple plot generation with chart data.

424. By Stevan Radaković

Add unique together contraint for name and image report on image
report charts.

423. By Stevan Radaković

Add display view and empty page.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'dashboard_app'
2=== added directory 'dashboard_app/migrations'
3=== added file 'dashboard_app/migrations/0032_auto__add_unique_imagereportchart_image_report_name.py'
4--- dashboard_app/migrations/0032_auto__add_unique_imagereportchart_image_report_name.py 1970-01-01 00:00:00 +0000
5+++ dashboard_app/migrations/0032_auto__add_unique_imagereportchart_image_report_name.py 2013-09-23 10:56:27 +0000
6@@ -0,0 +1,306 @@
7+# -*- coding: utf-8 -*-
8+import datetime
9+from south.db import db
10+from south.v2 import SchemaMigration
11+from django.db import models
12+
13+
14+class Migration(SchemaMigration):
15+
16+ def forwards(self, orm):
17+ # Adding unique constraint on 'ImageReportChart', fields ['image_report', 'name']
18+ db.create_unique('dashboard_app_imagereportchart', ['image_report_id', 'name'])
19+
20+
21+ def backwards(self, orm):
22+ # Removing unique constraint on 'ImageReportChart', fields ['image_report', 'name']
23+ db.delete_unique('dashboard_app_imagereportchart', ['image_report_id', 'name'])
24+
25+
26+ models = {
27+ 'auth.group': {
28+ 'Meta': {'object_name': 'Group'},
29+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
31+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
32+ },
33+ 'auth.permission': {
34+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
35+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
36+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
37+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
39+ },
40+ 'auth.user': {
41+ 'Meta': {'object_name': 'User'},
42+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
43+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
44+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
45+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
46+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
47+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
48+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
49+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
50+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
51+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
52+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
53+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
54+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
55+ },
56+ 'contenttypes.contenttype': {
57+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
58+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
59+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
60+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
61+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
62+ },
63+ 'dashboard_app.attachment': {
64+ 'Meta': {'object_name': 'Attachment'},
65+ 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
66+ 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
67+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
68+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69+ 'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
70+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
71+ 'public_url': ('django.db.models.fields.URLField', [], {'max_length': '512', 'blank': 'True'})
72+ },
73+ 'dashboard_app.bundle': {
74+ 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
75+ '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
76+ '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
77+ 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
78+ 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
79+ 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
80+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
81+ 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
82+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
83+ 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
84+ },
85+ 'dashboard_app.bundledeserializationerror': {
86+ 'Meta': {'object_name': 'BundleDeserializationError'},
87+ 'bundle': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'deserialization_error'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.Bundle']"}),
88+ 'error_message': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
89+ 'traceback': ('django.db.models.fields.TextField', [], {'max_length': '32768'})
90+ },
91+ 'dashboard_app.bundlestream': {
92+ 'Meta': {'object_name': 'BundleStream'},
93+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
94+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
96+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
97+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
98+ 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
99+ 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
100+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
101+ },
102+ 'dashboard_app.hardwaredevice': {
103+ 'Meta': {'object_name': 'HardwareDevice'},
104+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
105+ 'device_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
106+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
107+ },
108+ 'dashboard_app.image': {
109+ 'Meta': {'object_name': 'Image'},
110+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'to': "orm['dashboard_app.TestRunFilter']"}),
111+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
112+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024'})
113+ },
114+ 'dashboard_app.imagechartfilter': {
115+ 'Meta': {'object_name': 'ImageChartFilter'},
116+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestRunFilter']", 'null': 'True', 'on_delete': 'models.SET_NULL'}),
117+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
118+ 'image_chart': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.ImageReportChart']"}),
119+ 'representation': ('django.db.models.fields.CharField', [], {'default': "'lines'", 'max_length': '20'})
120+ },
121+ 'dashboard_app.imagecharttest': {
122+ 'Meta': {'unique_together': "(('image_chart_filter', 'test'),)", 'object_name': 'ImageChartTest'},
123+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
124+ 'image_chart_filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.ImageChartFilter']"}),
125+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
126+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.Test']"})
127+ },
128+ 'dashboard_app.imagecharttestcase': {
129+ 'Meta': {'unique_together': "(('image_chart_filter', 'test_case'),)", 'object_name': 'ImageChartTestCase'},
130+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131+ 'image_chart_filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.ImageChartFilter']"}),
132+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
133+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestCase']"})
134+ },
135+ 'dashboard_app.imagereport': {
136+ 'Meta': {'object_name': 'ImageReport'},
137+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
138+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
139+ 'is_published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
140+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '1024'})
141+ },
142+ 'dashboard_app.imagereportchart': {
143+ 'Meta': {'unique_together': "(('image_report', 'name'),)", 'object_name': 'ImageReportChart'},
144+ 'chart_type': ('django.db.models.fields.CharField', [], {'default': "'pass/fail'", 'max_length': '20'}),
145+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
146+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
147+ 'image_report': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['dashboard_app.ImageReport']"}),
148+ 'is_data_table_visible': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
149+ 'is_interactive': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
150+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
151+ 'target_goal': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '10', 'decimal_places': '5', 'blank': 'True'})
152+ },
153+ 'dashboard_app.imageset': {
154+ 'Meta': {'object_name': 'ImageSet'},
155+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
156+ 'images': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.Image']", 'symmetrical': 'False'}),
157+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
158+ },
159+ 'dashboard_app.launchpadbug': {
160+ 'Meta': {'object_name': 'LaunchpadBug'},
161+ 'bug_id': ('django.db.models.fields.PositiveIntegerField', [], {'unique': 'True'}),
162+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
163+ 'test_runs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'launchpad_bugs'", 'symmetrical': 'False', 'to': "orm['dashboard_app.TestRun']"})
164+ },
165+ 'dashboard_app.namedattribute': {
166+ 'Meta': {'unique_together': "(('object_id', 'name'),)", 'object_name': 'NamedAttribute'},
167+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
168+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
169+ 'name': ('django.db.models.fields.TextField', [], {}),
170+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
171+ 'value': ('django.db.models.fields.TextField', [], {})
172+ },
173+ 'dashboard_app.pmqabundlestream': {
174+ 'Meta': {'object_name': 'PMQABundleStream'},
175+ 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.BundleStream']"}),
176+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
177+ },
178+ 'dashboard_app.softwarepackage': {
179+ 'Meta': {'unique_together': "(('name', 'version'),)", 'object_name': 'SoftwarePackage'},
180+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
181+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
182+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
183+ },
184+ 'dashboard_app.softwarepackagescratch': {
185+ 'Meta': {'object_name': 'SoftwarePackageScratch'},
186+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
187+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
188+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
189+ },
190+ 'dashboard_app.softwaresource': {
191+ 'Meta': {'object_name': 'SoftwareSource'},
192+ 'branch_revision': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
193+ 'branch_url': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
194+ 'branch_vcs': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
195+ 'commit_timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
196+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
197+ 'project_name': ('django.db.models.fields.CharField', [], {'max_length': '32'})
198+ },
199+ 'dashboard_app.tag': {
200+ 'Meta': {'object_name': 'Tag'},
201+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
202+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '256'})
203+ },
204+ 'dashboard_app.test': {
205+ 'Meta': {'object_name': 'Test'},
206+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
207+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
208+ 'test_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '1024'})
209+ },
210+ 'dashboard_app.testcase': {
211+ 'Meta': {'unique_together': "(('test', 'test_case_id'),)", 'object_name': 'TestCase'},
212+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
213+ 'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
214+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_cases'", 'to': "orm['dashboard_app.Test']"}),
215+ 'test_case_id': ('django.db.models.fields.TextField', [], {}),
216+ 'units': ('django.db.models.fields.TextField', [], {'blank': 'True'})
217+ },
218+ 'dashboard_app.testdefinition': {
219+ 'Meta': {'object_name': 'TestDefinition'},
220+ 'content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
221+ 'description': ('django.db.models.fields.TextField', [], {}),
222+ 'environment': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
223+ 'format': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
224+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
225+ 'location': ('django.db.models.fields.CharField', [], {'default': "'LOCAL'", 'max_length': '64'}),
226+ 'mime_type': ('django.db.models.fields.CharField', [], {'default': "'text/plain'", 'max_length': '64'}),
227+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}),
228+ 'target_dev_types': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
229+ 'target_os': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
230+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
231+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '256'})
232+ },
233+ 'dashboard_app.testresult': {
234+ 'Meta': {'ordering': "('_order',)", 'object_name': 'TestResult'},
235+ '_order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
236+ 'filename': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
237+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
238+ 'lineno': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
239+ 'measurement': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '20', 'decimal_places': '10', 'blank': 'True'}),
240+ 'message': ('django.db.models.fields.TextField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
241+ 'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
242+ 'relative_index': ('django.db.models.fields.PositiveIntegerField', [], {}),
243+ 'result': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
244+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'test_results'", 'null': 'True', 'to': "orm['dashboard_app.TestCase']"}),
245+ 'test_run': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_results'", 'to': "orm['dashboard_app.TestRun']"}),
246+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
247+ },
248+ 'dashboard_app.testrun': {
249+ 'Meta': {'ordering': "['-import_assigned_date']", 'object_name': 'TestRun'},
250+ 'analyzer_assigned_date': ('django.db.models.fields.DateTimeField', [], {}),
251+ 'analyzer_assigned_uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '36'}),
252+ 'bundle': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Bundle']"}),
253+ 'devices': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.HardwareDevice']"}),
254+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
255+ 'import_assigned_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
256+ 'microseconds': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}),
257+ 'packages': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwarePackage']"}),
258+ 'sources': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.SoftwareSource']"}),
259+ 'sw_image_desc': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
260+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'test_runs'", 'blank': 'True', 'to': "orm['dashboard_app.Tag']"}),
261+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'test_runs'", 'to': "orm['dashboard_app.Test']"}),
262+ 'time_check_performed': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
263+ },
264+ 'dashboard_app.testrundenormalization': {
265+ 'Meta': {'object_name': 'TestRunDenormalization'},
266+ 'count_fail': ('django.db.models.fields.PositiveIntegerField', [], {}),
267+ 'count_pass': ('django.db.models.fields.PositiveIntegerField', [], {}),
268+ 'count_skip': ('django.db.models.fields.PositiveIntegerField', [], {}),
269+ 'count_unknown': ('django.db.models.fields.PositiveIntegerField', [], {}),
270+ 'test_run': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'denormalization'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['dashboard_app.TestRun']"})
271+ },
272+ 'dashboard_app.testrunfilter': {
273+ 'Meta': {'unique_together': "(('owner', 'name'),)", 'object_name': 'TestRunFilter'},
274+ 'build_number_attribute': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
275+ 'bundle_streams': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dashboard_app.BundleStream']", 'symmetrical': 'False'}),
276+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
277+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '1024'}),
278+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
279+ 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
280+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['auth.User']"})
281+ },
282+ 'dashboard_app.testrunfilterattribute': {
283+ 'Meta': {'object_name': 'TestRunFilterAttribute'},
284+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['dashboard_app.TestRunFilter']"}),
285+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
286+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
287+ 'value': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
288+ },
289+ 'dashboard_app.testrunfiltersubscription': {
290+ 'Meta': {'unique_together': "(('user', 'filter'),)", 'object_name': 'TestRunFilterSubscription'},
291+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dashboard_app.TestRunFilter']"}),
292+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
293+ 'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
294+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
295+ },
296+ 'dashboard_app.testrunfiltertest': {
297+ 'Meta': {'object_name': 'TestRunFilterTest'},
298+ 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tests'", 'to': "orm['dashboard_app.TestRunFilter']"}),
299+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
300+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
301+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.Test']"})
302+ },
303+ 'dashboard_app.testrunfiltertestcase': {
304+ 'Meta': {'object_name': 'TestRunFilterTestCase'},
305+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
306+ 'index': ('django.db.models.fields.PositiveIntegerField', [], {}),
307+ 'test': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'cases'", 'to': "orm['dashboard_app.TestRunFilterTest']"}),
308+ 'test_case': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['dashboard_app.TestCase']"})
309+ }
310+ }
311+
312+ complete_apps = ['dashboard_app']
313\ No newline at end of file
314
315=== added file 'dashboard_app/models.py.OTHER'
316--- dashboard_app/models.py.OTHER 1970-01-01 00:00:00 +0000
317+++ dashboard_app/models.py.OTHER 2013-09-23 10:56:27 +0000
318@@ -0,0 +1,2089 @@
319+# Copyright (C) 2010, 2011 Linaro Limited
320+#
321+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
322+#
323+# This file is part of Launch Control.
324+#
325+# Launch Control is free software: you can redistribute it and/or modify
326+# it under the terms of the GNU Affero General Public License version 3
327+# as published by the Free Software Foundation
328+#
329+# Launch Control is distributed in the hope that it will be useful,
330+# but WITHOUT ANY WARRANTY; without even the implied warranty of
331+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
332+# GNU General Public License for more details.
333+#
334+# You should have received a copy of the GNU Affero General Public License
335+# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
336+
337+"""
338+Database models of the Dashboard application
339+"""
340+
341+import datetime
342+import errno
343+import gzip
344+import hashlib
345+import logging
346+import os
347+import simplejson
348+import traceback
349+import contextlib
350+
351+from django.conf import settings
352+from django.contrib.auth.models import User
353+from django.contrib.contenttypes import generic
354+from django.contrib.contenttypes.models import ContentType
355+from django.contrib.sites.models import Site
356+from django.core.exceptions import ImproperlyConfigured, ValidationError
357+from django.core.files import locks, File
358+from django.core.files.storage import FileSystemStorage
359+from django.core.mail import send_mail
360+from django.core.urlresolvers import reverse
361+from django.db import models
362+from django.db.models.fields import FieldDoesNotExist
363+from django.db.models.signals import post_delete
364+from django.dispatch import receiver
365+from django.template import Template, Context
366+from django.template.defaultfilters import filesizeformat
367+from django.template.loader import render_to_string
368+from django.utils.translation import ugettext as _
369+from django.utils.translation import ungettext
370+
371+from django_restricted_resource.models import RestrictedResource
372+from linaro_dashboard_bundle.io import DocumentIO
373+
374+from dashboard_app.helpers import BundleDeserializer
375+from dashboard_app.managers import BundleManager, TestRunDenormalizationManager
376+from dashboard_app.repositories import RepositoryItem
377+from dashboard_app.repositories.data_report import DataReportRepository
378+from dashboard_app.repositories.data_view import DataViewRepository
379+from dashboard_app.signals import bundle_was_deserialized
380+
381+
382+# Fix some django issues we ran into
383+from dashboard_app.patches import patch
384+patch()
385+
386+
387+def _help_max_length(max_length):
388+ return ungettext(
389+ u"Maximum length: {0} character",
390+ u"Maximum length: {0} characters",
391+ max_length).format(max_length)
392+
393+
394+class SoftwarePackage(models.Model):
395+ """
396+ Model for software packages.
397+ """
398+ name = models.CharField(
399+ max_length = 128,
400+ verbose_name = _(u"Package name"),
401+ help_text = _help_max_length(128))
402+
403+ version = models.CharField(
404+ max_length = 128,
405+ verbose_name = _(u"Package version"),
406+ help_text = _help_max_length(128))
407+
408+ class Meta:
409+ unique_together = (('name', 'version'))
410+
411+ def __unicode__(self):
412+ return _(u"{name} {version}").format(
413+ name = self.name,
414+ version = self.version)
415+
416+ @property
417+ def link_to_packages_ubuntu_com(self):
418+ return u"http://packages.ubuntu.com/{name}".format(name=self.name)
419+
420+
421+class SoftwarePackageScratch(models.Model):
422+ """
423+ Staging area for SoftwarePackage data.
424+
425+ The code that keeps SoftwarePackage dumps data into here before more
426+ carefully inserting it into the real SoftwarePackage table.
427+
428+ No data should ever be committed in this table. It would be a temporary
429+ table, but oddities in how the sqlite DB-API wrapper handles transactions
430+ makes this impossible.
431+ """
432+ name = models.CharField(max_length=128)
433+ version = models.CharField(max_length=128)
434+
435+
436+class NamedAttribute(models.Model):
437+ """
438+ Model for adding generic named attributes
439+ to arbitrary other model instances.
440+
441+ Example:
442+ class Foo(Model):
443+ attributes = generic.GenericRelation(NamedAttribute)
444+ """
445+ name = models.TextField()
446+
447+ value = models.TextField()
448+
449+ # Content type plumbing
450+ content_type = models.ForeignKey(ContentType)
451+ object_id = models.PositiveIntegerField()
452+ content_object = generic.GenericForeignKey('content_type', 'object_id')
453+
454+ def __unicode__(self):
455+ return _(u"{name}: {value}").format(
456+ name = self.name,
457+ value = self.value)
458+
459+ class Meta:
460+ unique_together = (('object_id', 'name'))
461+
462+
463+class HardwareDevice(models.Model):
464+ """
465+ Model for hardware devices
466+
467+ All devices are simplified into an instance of pre-defined class
468+ with arbitrary key-value attributes.
469+ """
470+ device_type = models.CharField(
471+ choices = (
472+ (u"device.cpu", _(u"CPU")),
473+ (u"device.mem", _(u"Memory")),
474+ (u"device.usb", _(u"USB device")),
475+ (u"device.pci", _(u"PCI device")),
476+ (u"device.board", _(u"Board/Motherboard"))),
477+ help_text = _(u"One of pre-defined device types"),
478+ max_length = 32,
479+ verbose_name = _(u"Device Type"),
480+ )
481+
482+ description = models.CharField(
483+ help_text = _(u"Human readable device summary.") + " " + _help_max_length(256),
484+ max_length = 256,
485+ verbose_name = _(u"Description"),
486+ )
487+
488+ attributes = generic.GenericRelation(NamedAttribute)
489+
490+ def __unicode__(self):
491+ return self.description
492+
493+
494+class BundleStream(RestrictedResource):
495+ """
496+ Model for "streams" of bundles.
497+
498+ Basically it's a named collection of bundles, like directory just
499+ without the nesting. A simple ACL scheme is also supported,
500+ a bundle may be uploaded by:
501+ - specific user when user field is set
502+ - users of a specific group when group field is set
503+ - anyone when neither user nor group is set
504+ """
505+ PATHNAME_ANONYMOUS = "anonymous"
506+ PATHNAME_PUBLIC = "public"
507+ PATHNAME_PRIVATE = "private"
508+ PATHNAME_PERSONAL = "personal"
509+ PATHNAME_TEAM = "team"
510+
511+ slug = models.CharField(
512+ blank = True,
513+ help_text = (_(u"Name that you will use when uploading bundles.")
514+ + " " + _help_max_length(64)),
515+ max_length = 64,
516+ verbose_name = _(u"Slug"),
517+ )
518+
519+ name = models.CharField(
520+ blank = True,
521+ help_text = _help_max_length(64),
522+ max_length = 64,
523+ verbose_name = _(u"Name"),
524+ )
525+
526+ pathname = models.CharField(
527+ max_length = 128,
528+ editable = False,
529+ unique = True,
530+ )
531+
532+ is_anonymous = models.BooleanField()
533+
534+ def __unicode__(self):
535+ return self.pathname
536+
537+ @models.permalink
538+ def get_absolute_url(self):
539+ return ("dashboard_app.views.bundle_list", [self.pathname])
540+
541+ def get_test_run_count(self):
542+ return TestRun.objects.filter(bundle__bundle_stream=self).count()
543+
544+ def clean(self):
545+ if self.is_anonymous and not self.is_public:
546+ raise ValidationError(
547+ 'Anonymous streams must be public')
548+ return super(BundleStream, self).clean()
549+
550+ def save(self, *args, **kwargs):
551+ """
552+ Save this instance.
553+
554+ Calls self.clean() to ensure that constraints are met.
555+ Updates pathname to reflect user/group/slug changes.
556+ """
557+ self.pathname = self._calc_pathname()
558+ self.clean()
559+ return super(BundleStream, self).save(*args, **kwargs)
560+
561+ def _calc_pathname(self):
562+ """
563+ Pseudo pathname-like ID of this stream.
564+
565+ This pathname is user visible and will be presented to users
566+ when they want to interact with this bundle stream. The
567+ pathnames are unique and this is enforced at database level (the
568+ user and name are unique together).
569+ """
570+ if self.is_anonymous:
571+ parts = ['', self.PATHNAME_ANONYMOUS]
572+ else:
573+ if self.is_public:
574+ parts = ['', self.PATHNAME_PUBLIC]
575+ else:
576+ parts = ['', self.PATHNAME_PRIVATE]
577+ if self.user is not None:
578+ parts.append(self.PATHNAME_PERSONAL)
579+ parts.append(self.user.username)
580+ elif self.group is not None:
581+ parts.append(self.PATHNAME_TEAM)
582+ parts.append(self.group.name)
583+ if self.slug:
584+ parts.append(self.slug)
585+ parts.append('')
586+ return u"/".join(parts)
587+
588+ @classmethod
589+ def parse_pathname(cls, pathname):
590+ """
591+ Parse BundleStream pathname.
592+
593+ Returns user, group, slug, is_public, is_anonymous
594+ Raises ValueError if the pathname is not well formed
595+ """
596+ if not pathname.endswith('/'):
597+ pathname = pathname + '/'
598+ pathname_parts = pathname.split('/')
599+ if len(pathname_parts) < 3:
600+ raise ValueError("Pathname too short: %r" % pathname)
601+ if pathname_parts[0] != '':
602+ raise ValueError("Pathname must be absolute: %r" % pathname)
603+ if pathname_parts[1] == cls.PATHNAME_ANONYMOUS:
604+ user = None
605+ group = None
606+ slug = pathname_parts[2]
607+ correct_length = 2
608+ is_anonymous = True
609+ is_public = True
610+ else:
611+ is_anonymous = False
612+ if pathname_parts[1] == cls.PATHNAME_PUBLIC:
613+ is_public = True
614+ elif pathname_parts[1] == cls.PATHNAME_PRIVATE:
615+ is_public = False
616+ else:
617+ raise ValueError("Invalid pathname privacy designator:"
618+ " %r (full pathname: %r)" % (pathname_parts[1],
619+ pathname))
620+ if pathname_parts[2] == cls.PATHNAME_PERSONAL:
621+ if len(pathname_parts) < 4:
622+ raise ValueError("Pathname too short: %r" % pathname)
623+ user = pathname_parts[3]
624+ group = None
625+ slug = pathname_parts[4]
626+ correct_length = 4
627+ elif pathname_parts[2] == cls.PATHNAME_TEAM:
628+ if len(pathname_parts) < 4:
629+ raise ValueError("Pathname too short: %r" % pathname)
630+ user = None
631+ group = pathname_parts[3]
632+ slug = pathname_parts[4]
633+ correct_length = 4
634+ else:
635+ raise ValueError("Invalid pathname ownership designator:"
636+ " %r (full pathname %r)" % (pathname[2],
637+ pathname))
638+ if slug != '':
639+ correct_length += 1
640+ if pathname_parts[correct_length:] != ['']:
641+ raise ValueError("Junk after pathname: %r" % pathname)
642+ return user, group, slug, is_public, is_anonymous
643+
644+ def can_upload(self, user):
645+ """
646+ Return True if the user can upload bundles here
647+ """
648+ return self.is_anonymous or self.is_owned_by(user)
649+
650+
651+class GzipFileSystemStorage(FileSystemStorage):
652+
653+ def _open(self, name, mode='rb'):
654+ full_path = self.path(name)
655+ gzip_file = gzip.GzipFile(full_path, mode)
656+ gzip_file.name = full_path
657+ return File(gzip_file)
658+
659+ # This is a copy-paste-hack of FileSystemStorage._save
660+ def _save(self, name, content):
661+ full_path = self.path(name)
662+
663+ directory = os.path.dirname(full_path)
664+ if not os.path.exists(directory):
665+ os.makedirs(directory)
666+ elif not os.path.isdir(directory):
667+ raise IOError("%s exists and is not a directory." % directory)
668+
669+ # There's a potential race condition between get_available_name and
670+ # saving the file; it's possible that two threads might return the
671+ # same name, at which point all sorts of fun happens. So we need to
672+ # try to create the file, but if it already exists we have to go back
673+ # to get_available_name() and try again.
674+
675+ while True:
676+ try:
677+ # This fun binary flag incantation makes os.open throw an
678+ # OSError if the file already exists before we open it.
679+ fd = os.open(full_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0))
680+ # This line, and the use of gz_file.write below, are the
681+ # changes from the original version of this.
682+ gz_file = gzip.GzipFile(fileobj=os.fdopen(fd, 'wb'))
683+ try:
684+ locks.lock(fd, locks.LOCK_EX)
685+ for chunk in content.chunks():
686+ gz_file.write(chunk)
687+ finally:
688+ locks.unlock(fd)
689+ gz_file.close()
690+ except OSError, e:
691+ if e.errno == errno.EEXIST:
692+ # Ooops, the file exists. We need a new file name.
693+ name = self.get_available_name(name)
694+ full_path = self.path(name)
695+ else:
696+ raise
697+ else:
698+ # OK, the file save worked. Break out of the loop.
699+ break
700+
701+ if settings.FILE_UPLOAD_PERMISSIONS is not None:
702+ os.chmod(full_path, settings.FILE_UPLOAD_PERMISSIONS)
703+
704+ return name
705+
706+
707+class Bundle(models.Model):
708+ """
709+ Model for "Dashboard Bundles"
710+ """
711+ bundle_stream = models.ForeignKey(BundleStream,
712+ verbose_name = _(u"Stream"),
713+ related_name = 'bundles')
714+
715+ uploaded_by = models.ForeignKey(User,
716+ verbose_name = _(u"Uploaded by"),
717+ help_text = _(u"The user who submitted this bundle"),
718+ related_name = 'uploaded_bundles',
719+ null = True,
720+ blank = True)
721+
722+ uploaded_on = models.DateTimeField(
723+ verbose_name = _(u"Uploaded on"),
724+ editable = False,
725+ default = datetime.datetime.utcnow)
726+
727+ is_deserialized = models.BooleanField(
728+ verbose_name = _(u"Is deserialized"),
729+ help_text = _(u"Set when document has been analyzed and loaded"
730+ " into the database"),
731+ editable = False)
732+
733+ _raw_content = models.FileField(
734+ verbose_name = _(u"Content"),
735+ help_text = _(u"Document in Dashboard Bundle Format 1.0"),
736+ upload_to = 'bundles',
737+ null = True,
738+ db_column = 'content')
739+
740+ _gz_content = models.FileField(
741+ verbose_name = _(u"Compressed content"),
742+ help_text = _(u"Compressed document in Dashboard Bundle Format 1.0"),
743+ upload_to = 'compressed-bundles',
744+ null = True,
745+ db_column = 'gz_content',
746+ storage = GzipFileSystemStorage())
747+
748+ def _get_content(self):
749+ r = self._gz_content
750+ if not r:
751+ return self._raw_content
752+ else:
753+ return r
754+
755+ content = property(_get_content)
756+
757+ def compress(self):
758+ c = self._raw_content
759+ self._gz_content.save(c.name, c)
760+ c.delete()
761+
762+ content_sha1 = models.CharField(
763+ editable = False,
764+ max_length = 40,
765+ null = True,
766+ unique = True)
767+
768+ content_filename = models.CharField(
769+ verbose_name = _(u"Content file name"),
770+ help_text = _(u"Name of the originally uploaded bundle"),
771+ max_length = 256)
772+
773+ objects = BundleManager()
774+
775+ def __unicode__(self):
776+ return _(u"Bundle {0}").format(self.content_sha1)
777+
778+ class Meta:
779+ ordering = ['-uploaded_on']
780+
781+ @models.permalink
782+ def get_absolute_url(self):
783+ return ("dashboard_app.views.bundle_detail", [self.bundle_stream.pathname, self.content_sha1])
784+
785+ def get_permalink(self):
786+ return reverse("dashboard_app.views.redirect_to_bundle", args=[self.content_sha1])
787+
788+ def save(self, *args, **kwargs):
789+ if self.content:
790+ try:
791+ self.content.open('rb')
792+ sha1 = hashlib.sha1()
793+ for chunk in self.content.chunks():
794+ sha1.update(chunk)
795+ self.content_sha1 = sha1.hexdigest()
796+ finally:
797+ self.content.close()
798+ return super(Bundle, self).save(*args, **kwargs)
799+
800+ def deserialize(self, prefer_evolution=False):
801+ """
802+ Deserialize the contents of this bundle.
803+
804+ The actual implementation is _do_serialize() this function
805+ catches any exceptions it might throw and converts them to
806+ BundleDeserializationError instance. Any previous import errors are
807+ overwritten.
808+
809+ Successful import also discards any previous import errors and
810+ sets is_deserialized to True.
811+ """
812+ if self.is_deserialized:
813+ return
814+ try:
815+ self._do_deserialize(prefer_evolution)
816+ except Exception as ex:
817+ import_error = BundleDeserializationError.objects.get_or_create(
818+ bundle=self)[0]
819+ import_error.error_message = str(ex)
820+ import_error.traceback = traceback.format_exc()
821+ import_error.save()
822+ else:
823+ try:
824+ self.deserialization_error.delete()
825+ except BundleDeserializationError.DoesNotExist:
826+ pass
827+ self.is_deserialized = True
828+ self.save()
829+ bundle_was_deserialized.send_robust(sender=self, bundle=self)
830+
831+ def _do_deserialize(self, prefer_evolution):
832+ """
833+ Deserialize this bundle or raise an exception
834+ """
835+ helper = BundleDeserializer()
836+ helper.deserialize(self, prefer_evolution)
837+
838+ def get_summary_results(self):
839+ if self.is_deserialized:
840+ stats = TestResult.objects.filter(
841+ test_run__bundle = self).values(
842+ 'result').annotate(
843+ count=models.Count('result'))
844+ result = dict([
845+ (TestResult.RESULT_MAP[item['result']], item['count'])
846+ for item in stats])
847+ result['total'] = sum(result.values())
848+ return result
849+
850+ def delete_files(self, save=False):
851+ """
852+ Delete all files related to this bundle.
853+
854+ This is currently used in test code to clean up after testing.
855+ """
856+ self.content.delete(save=save)
857+ for test_run in self.test_runs.all():
858+ for attachment in test_run.attachments.all():
859+ attachment.content.delete(save=save)
860+
861+ def get_sanitized_bundle(self):
862+ self.content.open()
863+ try:
864+ return SanitizedBundle(self.content)
865+ finally:
866+ self.content.close()
867+
868+ def get_document_format(self):
869+ try:
870+ self.content.open('rb')
871+ except IOError:
872+ return "unknown"
873+ else:
874+ try:
875+ fmt, doc = DocumentIO.load(self.content)
876+ return fmt
877+ finally:
878+ self.content.close()
879+
880+ def get_serialization_format(self):
881+ return "JSON"
882+
883+ def get_content_size(self):
884+ try:
885+ return filesizeformat(self.content.size)
886+ except OSError:
887+ return "unknown"
888+
889+
890+class SanitizedBundle(object):
891+
892+ def __init__(self, stream):
893+ try:
894+ self.bundle_json = simplejson.load(stream)
895+ self.deserialization_error = None
896+ except simplejson.JSONDeserializationError as ex:
897+ self.bundle_json = None
898+ self.deserialization_error = ex
899+ self.did_remove_attachments = False
900+ self._sanitize()
901+
902+ def get_human_readable_json(self):
903+ return simplejson.dumps(self.bundle_json, indent=4)
904+
905+ def _sanitize(self):
906+ for test_run in self.bundle_json.get("test_runs", []):
907+ attachments = test_run.get("attachments")
908+ if isinstance(attachments, list):
909+ for attachment in attachments:
910+ attachment["content"] = None
911+ self.did_remove_attachments = True
912+ elif isinstance(attachments, dict):
913+ for name in attachments:
914+ attachments[name] = None
915+ self.did_remove_attachments = True
916+
917+
918+class BundleDeserializationError(models.Model):
919+ """
920+ Model for representing errors encountered during bundle
921+ deserialization. There is one instance per bundle limit due to
922+ unique = True. There used to be a OneToOne field but it didn't work
923+ with databrowse application.
924+
925+ The relevant logic for managing this is in the Bundle.deserialize()
926+ """
927+
928+ bundle = models.OneToOneField(
929+ Bundle,
930+ primary_key = True,
931+ unique = True,
932+ related_name = 'deserialization_error'
933+ )
934+
935+ error_message = models.CharField(
936+ max_length = 1024
937+ )
938+
939+ traceback = models.TextField(
940+ max_length = 1 << 15,
941+ )
942+
943+ def __unicode__(self):
944+ return self.error_message
945+
946+
947+class Test(models.Model):
948+ """
949+ Model for representing tests.
950+
951+ Test is a collection of individual test cases.
952+ """
953+ test_id = models.CharField(
954+ max_length = 1024,
955+ verbose_name = _("Test ID"),
956+ unique = True)
957+
958+ name = models.CharField(
959+ blank = True,
960+ max_length = 1024,
961+ verbose_name = _(u"Name"))
962+
963+ def __unicode__(self):
964+ return self.name or self.test_id
965+
966+ @models.permalink
967+ def get_absolute_url(self):
968+ return ('dashboard_app.views.test_detail', [self.test_id])
969+
970+ def count_results_without_test_case(self):
971+ return TestResult.objects.filter(
972+ test_run__test=self,
973+ test_case=None).count()
974+
975+ def count_failures_without_test_case(self):
976+ return TestResult.objects.filter(
977+ test_run__test=self,
978+ test_case=None,
979+ result=TestResult.RESULT_FAIL).count()
980+
981+
982+class TestCase(models.Model):
983+ """
984+ Model for representing test cases.
985+
986+ Test case is a unique component of a specific test.
987+ Test cases allow for relating to test results.
988+ """
989+ test = models.ForeignKey(
990+ Test,
991+ related_name='test_cases')
992+
993+ test_case_id = models.TextField(
994+ verbose_name = _("Test case ID"))
995+
996+ name = models.TextField(
997+ blank = True,
998+ help_text = _help_max_length(100),
999+ verbose_name = _("Name"))
1000+
1001+ units = models.TextField(
1002+ blank = True,
1003+ help_text = (_("""Units in which measurement value should be
1004+ interpreted in, for example <q>ms</q>, <q>MB/s</q> etc.
1005+ There is no semantical meaning inferred from the value of
1006+ this field, free form text is allowed. <br/>""")
1007+ + _help_max_length(100)),
1008+ verbose_name = _("Units"))
1009+
1010+ class Meta:
1011+ unique_together = (('test', 'test_case_id'))
1012+
1013+ def __unicode__(self):
1014+ return self.name or self.test_case_id
1015+
1016+ @models.permalink
1017+ def get_absolute_url(self):
1018+ return ("dashboard_app.test_case.details", [self.test.test_id, self.test_case_id])
1019+
1020+ def count_failures(self):
1021+ return self.test_results.filter(result=TestResult.RESULT_FAIL).count()
1022+
1023+
1024+class TestDefinition(models.Model):
1025+ """
1026+ Model for representing test definitions.
1027+
1028+ Test Definition are in YAML format.
1029+ """
1030+ LOCATION_CHOICES = (
1031+ ('LOCAL', 'Local'),
1032+ ('URL', 'URL'),
1033+ ('GIT', 'GIT Repo'),
1034+ ('BZR', 'BZR Repo'),
1035+ )
1036+
1037+ name = models.CharField(
1038+ max_length = 512,
1039+ verbose_name = _("Name"),
1040+ unique = True,
1041+ help_text = _help_max_length(512))
1042+
1043+ version = models.CharField(
1044+ max_length=256,
1045+ verbose_name = _("Version"),
1046+ help_text = _help_max_length(256))
1047+
1048+ description = models.TextField(
1049+ verbose_name = _("Description"))
1050+
1051+ format = models.CharField(
1052+ max_length = 128,
1053+ verbose_name = _("Format"),
1054+ help_text = _help_max_length(128))
1055+
1056+ location = models.CharField(
1057+ max_length = 64,
1058+ verbose_name = _("Location"),
1059+ choices = LOCATION_CHOICES,
1060+ default = 'LOCAL')
1061+
1062+ url = models.CharField(
1063+ verbose_name = _(u"URL"),
1064+ max_length = 1024,
1065+ blank = False,
1066+ help_text = _help_max_length(1024))
1067+
1068+ environment = models.CharField(
1069+ max_length = 256,
1070+ verbose_name = _("Environment"),
1071+ help_text = _help_max_length(256))
1072+
1073+ target_os = models.CharField(
1074+ max_length = 512,
1075+ verbose_name = _("Operating Systems"),
1076+ help_text = _help_max_length(512))
1077+
1078+ target_dev_types = models.CharField(
1079+ max_length = 512,
1080+ verbose_name = _("Device types"),
1081+ help_text = _help_max_length(512))
1082+
1083+ content = models.FileField(
1084+ verbose_name = _(u"Upload Test Definition"),
1085+ help_text = _(u"Test definition file"),
1086+ upload_to = 'testdef',
1087+ blank = True,
1088+ null = True)
1089+
1090+ mime_type = models.CharField(
1091+ verbose_name = _(u"MIME type"),
1092+ default = 'text/plain',
1093+ max_length = 64,
1094+ help_text = _help_max_length(64))
1095+
1096+ def __unicode__(self):
1097+ return self.name
1098+
1099+
1100+class SoftwareSource(models.Model):
1101+ """
1102+ Model for representing source reference of a particular project
1103+ """
1104+
1105+ project_name = models.CharField(
1106+ max_length = 32,
1107+ help_text = _help_max_length(32),
1108+ verbose_name = _(u"Project Name"),
1109+ )
1110+ branch_url = models.CharField(
1111+ max_length = 256,
1112+ help_text = _help_max_length(256),
1113+ verbose_name = _(u"Branch URL"),
1114+ )
1115+ branch_vcs = models.CharField(
1116+ max_length = 10,
1117+ help_text = _help_max_length(10),
1118+ verbose_name = _(u"Branch VCS"),
1119+ )
1120+ branch_revision = models.CharField(
1121+ max_length = 128,
1122+ help_text = _help_max_length(128),
1123+ verbose_name = _(u"Branch Revision")
1124+ )
1125+ commit_timestamp = models.DateTimeField(
1126+ blank=True,
1127+ null=True,
1128+ help_text = _(u"Date and time of the commit (optional)"),
1129+ verbose_name = _(u"Commit Timestamp")
1130+ )
1131+
1132+ def __unicode__(self):
1133+ return _(u"{project_name} from branch {branch_url} at revision {branch_revision}").format(
1134+ project_name=self.project_name, branch_url=self.branch_url, branch_revision=self.branch_revision)
1135+
1136+ @property
1137+ def is_hosted_on_launchpad(self):
1138+ return self.branch_url.startswith("lp:")
1139+
1140+ @property
1141+ def is_tag_revision(self):
1142+ return self.branch_revision.startswith("tag:")
1143+
1144+ @property
1145+ def branch_tag(self):
1146+ if self.is_tag_revision:
1147+ return self.branch_revision[len("tag:"):]
1148+
1149+ @property
1150+ def link_to_project(self):
1151+ return "http://launchpad.net/{project_name}".format(project_name=self.project_name)
1152+
1153+ @property
1154+ def link_to_branch(self):
1155+ if self.is_hosted_on_launchpad:
1156+ return "http://launchpad.net/{branch_url}/".format(branch_url=self.branch_url[len("lp:"):])
1157+
1158+class TestRun(models.Model):
1159+ """
1160+ Model for representing test runs.
1161+
1162+ Test run is an act of running a Test in a certain context. The
1163+ context is described by the software and hardware environment. In
1164+ addition to those properties each test run can have arbitrary named
1165+ properties for additional context that is not reflected in the
1166+ database directly.
1167+
1168+ Test runs have global identity exists beyond the lifetime of
1169+ bundle that essentially encapsulates test run information should
1170+ store the UUID that was generated at the time the document is made.
1171+ the dashboard application. The software that prepares the dashboard
1172+ """
1173+
1174+ # Meta-data
1175+
1176+ bundle = models.ForeignKey(
1177+ Bundle,
1178+ related_name = 'test_runs',
1179+ )
1180+
1181+ test = models.ForeignKey(
1182+ Test,
1183+ related_name = 'test_runs',
1184+ )
1185+
1186+ analyzer_assigned_uuid = models.CharField(
1187+ help_text = _(u"You can use uuid.uuid1() to generate a value"),
1188+ max_length = 36,
1189+ unique = True,
1190+ verbose_name = _(u"Analyzer assigned UUID"),
1191+ )
1192+
1193+ analyzer_assigned_date = models.DateTimeField(
1194+ verbose_name = _(u"Analyzer assigned date"),
1195+ help_text = _(u"Time stamp when the log was processed by the log"
1196+ " analyzer"),
1197+ )
1198+
1199+ import_assigned_date = models.DateTimeField(
1200+ verbose_name = _(u"Import assigned date"),
1201+ help_text = _(u"Time stamp when the bundle was imported"),
1202+ auto_now_add = True,
1203+ )
1204+
1205+ time_check_performed = models.BooleanField(
1206+ verbose_name = _(u"Time check performed"),
1207+ help_text = _(u"Indicator on wether timestamps in the log file (and any "
1208+ "data derived from them) should be trusted.<br/>"
1209+ "Many pre-production or development devices do not "
1210+ "have a battery-powered RTC and it's not common for "
1211+ "development images not to synchronize time with "
1212+ "internet time servers.<br/>"
1213+ "This field allows us to track tests results that "
1214+ "<em>certainly</em> have correct time if we ever end up "
1215+ "with lots of tests results from 1972")
1216+ )
1217+
1218+ microseconds = models.BigIntegerField(
1219+ blank = True,
1220+ null = True
1221+ )
1222+
1223+ # Software Context
1224+
1225+ sw_image_desc = models.CharField(
1226+ blank = True,
1227+ max_length = 100,
1228+ verbose_name = _(u"Operating System Image"),
1229+ )
1230+
1231+ packages = models.ManyToManyField(
1232+ SoftwarePackage,
1233+ blank = True,
1234+ related_name = 'test_runs',
1235+ verbose_name = _(u"Software packages"),
1236+ )
1237+
1238+ sources = models.ManyToManyField(
1239+ SoftwareSource,
1240+ blank = True,
1241+ related_name = 'test_runs',
1242+ verbose_name = _(u"Software sources"),
1243+ )
1244+
1245+ # Hardware Context
1246+
1247+ devices = models.ManyToManyField(
1248+ HardwareDevice,
1249+ blank = True,
1250+ related_name = 'test_runs',
1251+ verbose_name = _(u"Hardware devices"),
1252+ )
1253+
1254+ # Attributes
1255+
1256+ attributes = generic.GenericRelation(NamedAttribute)
1257+
1258+ # Tags
1259+
1260+ tags = models.ManyToManyField(
1261+ "Tag",
1262+ blank=True,
1263+ related_name='test_runs',
1264+ verbose_name=_(u"Tags"))
1265+
1266+ # Attachments
1267+
1268+ attachments = generic.GenericRelation('Attachment')
1269+
1270+ def __unicode__(self):
1271+ return _(u"Test run {0}").format(self.analyzer_assigned_uuid)
1272+
1273+ @models.permalink
1274+ def get_absolute_url(self):
1275+ return ("dashboard_app.views.test_run_detail",
1276+ [self.bundle.bundle_stream.pathname,
1277+ self.bundle.content_sha1,
1278+ self.analyzer_assigned_uuid])
1279+
1280+ def get_permalink(self):
1281+ return reverse("dashboard_app.views.redirect_to_test_run", args=[self.analyzer_assigned_uuid])
1282+
1283+ def get_board(self):
1284+ """
1285+ Return an associated Board device, if any.
1286+ """
1287+ try:
1288+ return self.devices.filter(device_type="device.board").get()
1289+ except HardwareDevice.DoesNotExist:
1290+ pass
1291+ except HardwareDevice.MultipleObjectsReturned:
1292+ pass
1293+
1294+ def get_results(self):
1295+ """
1296+ Get all results efficiently
1297+ """
1298+ return self.test_results.select_related(
1299+ "test_case", # explicit join on test_case which might be NULL
1300+ "test_run", # explicit join on test run, needed by all the get_absolute_url() methods
1301+ "test_run__bundle", # explicit join on bundle
1302+ "test_run__bundle__bundle_stream", # explicit join on bundle stream
1303+ ).order_by("relative_index") # sort as they showed up in the bundle
1304+
1305+ def denormalize(self):
1306+ try:
1307+ self.denormalization
1308+ except TestRunDenormalization.DoesNotExist:
1309+ TestRunDenormalization.objects.create_from_test_run(self)
1310+
1311+ def _get_summary_results(self, factor=3):
1312+ stats = self.test_results.values('result').annotate(
1313+ count=models.Count('result')).order_by()
1314+ result = dict([
1315+ (TestResult.RESULT_MAP[item['result']], item['count'])
1316+ for item in stats])
1317+ result['total'] = sum(result.values())
1318+ result['total_multiplied'] = result['total'] * factor
1319+ return result
1320+
1321+ def get_summary_results(self):
1322+ if not hasattr(self, '_cached_summary_results'):
1323+ self._cached_summary_results = self._get_summary_results()
1324+ return self._cached_summary_results
1325+
1326+ # test_duration property
1327+
1328+ def _get_test_duration(self):
1329+ if self.microseconds is None:
1330+ return None
1331+ else:
1332+ return datetime.timedelta(microseconds = self.microseconds)
1333+
1334+ def _set_test_duration(self, duration):
1335+ if duration is None:
1336+ self.microseconds = None
1337+ else:
1338+ if not isinstance(duration, datetime.timedelta):
1339+ raise TypeError("duration must be a datetime.timedelta() instance")
1340+ self.microseconds = (
1341+ duration.microseconds +
1342+ (duration.seconds * 10 ** 6) +
1343+ (duration.days * 24 * 60 * 60 * 10 ** 6))
1344+
1345+ test_duration = property(_get_test_duration, _set_test_duration)
1346+
1347+ class Meta:
1348+ ordering = ['-import_assigned_date']
1349+
1350+ def show_device(self):
1351+ all_attributes = self.attributes.all()
1352+ for one_attributes in all_attributes:
1353+ if one_attributes.name == "target":
1354+ return one_attributes.value
1355+
1356+ for one_attributes in all_attributes:
1357+ if one_attributes.name == "target.hostname":
1358+ return one_attributes.value
1359+
1360+ for one_attributes in all_attributes:
1361+ if one_attributes.name == "target.device_type":
1362+ return one_attributes.value
1363+ return "Target Device"
1364+
1365+
1366+class TestRunDenormalization(models.Model):
1367+ """
1368+ Denormalized model for test run
1369+ """
1370+
1371+ test_run = models.OneToOneField(
1372+ TestRun,
1373+ primary_key=True,
1374+ related_name="denormalization")
1375+
1376+ count_pass = models.PositiveIntegerField(
1377+ null=False,
1378+ blank=False)
1379+
1380+ count_fail = models.PositiveIntegerField(
1381+ null=False,
1382+ blank=False)
1383+
1384+ count_skip = models.PositiveIntegerField(
1385+ null=False,
1386+ blank=False)
1387+
1388+ count_unknown = models.PositiveIntegerField(
1389+ null=False,
1390+ blank=False)
1391+
1392+ def count_all(self):
1393+ return (self.count_pass + self.count_fail + self.count_skip +
1394+ self.count_unknown)
1395+
1396+ objects = TestRunDenormalizationManager()
1397+
1398+
1399+class Attachment(models.Model):
1400+ """
1401+ Model for adding attachments to any other models.
1402+ """
1403+
1404+ content = models.FileField(
1405+ verbose_name = _(u"Content"),
1406+ help_text = _(u"Attachment content"),
1407+ upload_to = 'attachments',
1408+ null = True)
1409+
1410+ content_filename = models.CharField(
1411+ verbose_name = _(u"Content file name"),
1412+ help_text = _(u"Name of the original attachment"),
1413+ max_length = 256)
1414+
1415+ mime_type = models.CharField(
1416+ verbose_name = _(u"MIME type"),
1417+ max_length = 64)
1418+
1419+ public_url = models.URLField(
1420+ verbose_name = _(u"Public URL"),
1421+ max_length = 512,
1422+ blank = True)
1423+
1424+ # Content type plumbing
1425+ content_type = models.ForeignKey(ContentType)
1426+ object_id = models.PositiveIntegerField()
1427+ content_object = generic.GenericForeignKey('content_type', 'object_id')
1428+
1429+ def __unicode__(self):
1430+ return self.content_filename
1431+
1432+ def is_test_run_attachment(self):
1433+ if (self.content_type.app_label == 'dashboard_app' and
1434+ self.content_type.model == 'testrun'):
1435+ return True
1436+
1437+ def is_test_result_attachment(self):
1438+ if (self.content_type.app_label == 'dashboard_app' and
1439+ self.content_type.model == 'testresult'):
1440+ return True
1441+
1442+ @property
1443+ def test_run(self):
1444+ if self.is_test_run_attachment():
1445+ return self.content_object
1446+
1447+ @property
1448+ def test_result(self):
1449+ if self.is_test_result_attachment():
1450+ return self.content_object
1451+
1452+ @property
1453+ def bundle(self):
1454+ if self.is_test_result_attachment():
1455+ run = self.test_result.test_run
1456+ elif self.is_test_run_attachment():
1457+ run = self.test_run
1458+ return run.bundle
1459+
1460+ def get_content_size(self):
1461+ try:
1462+ return filesizeformat(self.content.size)
1463+ except OSError:
1464+ return "unknown size"
1465+
1466+ @models.permalink
1467+ def get_download_url(self):
1468+ return ("dashboard_app.views.attachment_download",
1469+ [self.pk])
1470+
1471+ @models.permalink
1472+ def get_view_url(self):
1473+ return ("dashboard_app.views.attachment_view",
1474+ [self.pk])
1475+
1476+ def is_viewable(self):
1477+ return self.mime_type in ['text/plain']
1478+
1479+
1480+class TestResult(models.Model):
1481+ """
1482+ Model for representing test results.
1483+ """
1484+
1485+ RESULT_PASS = 0
1486+ RESULT_FAIL = 1
1487+ RESULT_SKIP = 2
1488+ RESULT_UNKNOWN = 3
1489+
1490+ RESULT_MAP = {
1491+ RESULT_PASS: 'pass',
1492+ RESULT_FAIL: 'fail',
1493+ RESULT_SKIP: 'skip',
1494+ RESULT_UNKNOWN: 'unknown'
1495+ }
1496+
1497+ # Context information
1498+
1499+ test_run = models.ForeignKey(
1500+ TestRun,
1501+ related_name = "test_results"
1502+ )
1503+
1504+ test_case = models.ForeignKey(
1505+ TestCase,
1506+ related_name = "test_results",
1507+ null = True,
1508+ blank = True
1509+ )
1510+
1511+ @property
1512+ def test(self):
1513+ return self.test_run.test
1514+
1515+ # Core attributes
1516+
1517+ result = models.PositiveSmallIntegerField(
1518+ verbose_name = _(u"Result"),
1519+ help_text = _(u"Result classification to pass/fail group"),
1520+ choices = (
1521+ (RESULT_PASS, _(u"Test passed")),
1522+ (RESULT_FAIL, _(u"Test failed")),
1523+ (RESULT_SKIP, _(u"Test skipped")),
1524+ (RESULT_UNKNOWN, _(u"Unknown outcome")))
1525+ )
1526+
1527+ measurement = models.DecimalField(
1528+ blank = True,
1529+ decimal_places = 10,
1530+ help_text = _(u"Arbitrary value that was measured as a part of this test."),
1531+ max_digits = 20,
1532+ null = True,
1533+ verbose_name = _(u"Measurement"),
1534+ )
1535+
1536+ # Misc attributes
1537+
1538+ filename = models.CharField(
1539+ blank = True,
1540+ max_length = 1024,
1541+ null = True,
1542+ )
1543+
1544+ lineno = models.PositiveIntegerField(
1545+ blank = True,
1546+ null = True
1547+ )
1548+
1549+ message = models.TextField(
1550+ blank = True,
1551+ max_length = 1024,
1552+ null = True
1553+ )
1554+
1555+ microseconds = models.BigIntegerField(
1556+ blank = True,
1557+ null = True
1558+ )
1559+
1560+ timestamp = models.DateTimeField(
1561+ blank = True,
1562+ null = True
1563+ )
1564+
1565+ relative_index = models.PositiveIntegerField(
1566+ help_text = _(u"The relative order of test results in one test run")
1567+ )
1568+
1569+ def __unicode__(self):
1570+ return "Result {0}/{1}".format(self.test_run.analyzer_assigned_uuid, self.relative_index)
1571+
1572+ @models.permalink
1573+ def get_absolute_url(self):
1574+ return ("dashboard_app.views.test_result_detail", [
1575+ self.test_run.bundle.bundle_stream.pathname,
1576+ self.test_run.bundle.content_sha1,
1577+ self.test_run.analyzer_assigned_uuid,
1578+ self.relative_index,
1579+ ])
1580+
1581+ def get_permalink(self):
1582+ return reverse("dashboard_app.views.redirect_to_test_result",
1583+ args=[self.test_run.analyzer_assigned_uuid,
1584+ self.relative_index])
1585+
1586+ @property
1587+ def result_code(self):
1588+ """
1589+ Stable textual result code that does not depend on locale
1590+ """
1591+ return self.RESULT_MAP[self.result]
1592+
1593+ # units (via test case)
1594+
1595+ @property
1596+ def units(self):
1597+ try:
1598+ return self.test_case.units
1599+ except TestCase.DoesNotExist:
1600+ return None
1601+
1602+ # Attributes
1603+
1604+ attributes = generic.GenericRelation(NamedAttribute)
1605+
1606+ # Attachments
1607+
1608+ attachments = generic.GenericRelation(Attachment)
1609+
1610+ # Duration property
1611+
1612+ def _get_duration(self):
1613+ if self.microseconds is None:
1614+ return None
1615+ else:
1616+ return datetime.timedelta(microseconds = self.microseconds)
1617+
1618+ def _set_duration(self, duration):
1619+ if duration is None:
1620+ self.microseconds = None
1621+ else:
1622+ if not isinstance(duration, datetime.timedelta):
1623+ raise TypeError("duration must be a datetime.timedelta() instance")
1624+ self.microseconds = (
1625+ duration.microseconds +
1626+ (duration.seconds * 10 ** 6) +
1627+ (duration.days * 24 * 60 * 60 * 10 ** 6))
1628+
1629+ duration = property(_get_duration, _set_duration)
1630+
1631+ def related_attachment_available(self):
1632+ """
1633+ Check if there is a log file attached to the test run that has
1634+ the same filename as log filename recorded in the result here.
1635+ """
1636+ try:
1637+ self.related_attachment()
1638+ return True
1639+ except Attachment.DoesNotExist:
1640+ return False
1641+
1642+ def related_attachment(self):
1643+ return self.test_run.attachments.get(content_filename=self.filename)
1644+
1645+ class Meta:
1646+ ordering = ['relative_index']
1647+ order_with_respect_to = 'test_run'
1648+
1649+
1650+class DataView(RepositoryItem):
1651+ """
1652+ Data view, a container for SQL query and optional arguments
1653+ """
1654+
1655+ repository = DataViewRepository()
1656+
1657+ def __init__(self, name, backend_queries, arguments, documentation, summary):
1658+ self.name = name
1659+ self.backend_queries = backend_queries
1660+ self.arguments = arguments
1661+ self.documentation = documentation
1662+ self.summary = summary
1663+
1664+ def __unicode__(self):
1665+ return self.name
1666+
1667+ def __repr__(self):
1668+ return "<DataView name=%r>" % (self.name,)
1669+
1670+ @models.permalink
1671+ def get_absolute_url(self):
1672+ return ("dashboard_app.views.data_view_detail", [self.name])
1673+
1674+ def _get_connection_backend_name(self, connection):
1675+ backend = str(type(connection))
1676+ if "sqlite" in backend:
1677+ return "sqlite"
1678+ elif "postgresql" in backend:
1679+ return "postgresql"
1680+ else:
1681+ return ""
1682+
1683+ def get_backend_specific_query(self, connection):
1684+ """
1685+ Return BackendSpecificQuery for the specified connection
1686+ """
1687+ sql_backend_name = self._get_connection_backend_name(connection)
1688+ try:
1689+ return self.backend_queries[sql_backend_name]
1690+ except KeyError:
1691+ return self.backend_queries.get(None, None)
1692+
1693+ def lookup_argument(self, name):
1694+ """
1695+ Return Argument with the specified name
1696+
1697+ Raises LookupError if the argument cannot be found
1698+ """
1699+ for argument in self.arguments:
1700+ if argument.name == name:
1701+ return argument
1702+ raise LookupError(name)
1703+
1704+ @classmethod
1705+ def get_connection(cls):
1706+ """
1707+ Get the appropriate connection for data views
1708+ """
1709+ from django.db import connection, connections
1710+ from django.db.utils import ConnectionDoesNotExist
1711+ try:
1712+ return connections['dataview']
1713+ except ConnectionDoesNotExist:
1714+ logging.warning("dataview-specific database connection not available, dataview query is NOT sandboxed")
1715+ return connection # NOTE: it's connection not connectionS (the default connection)
1716+
1717+ def __call__(self, connection, **arguments):
1718+ # Check if arguments have any bogus names
1719+ valid_arg_names = frozenset([argument.name for argument in self.arguments])
1720+ for arg_name in arguments:
1721+ if arg_name not in valid_arg_names:
1722+ raise TypeError("Data view %s has no argument %r" % (self.name, arg_name))
1723+ # Get the SQL template for our database connection
1724+ query = self.get_backend_specific_query(connection)
1725+ if query is None:
1726+ raise LookupError("Specified data view has no SQL implementation "
1727+ "for current database")
1728+ # Replace SQL aruments with django placeholders (connection agnostic)
1729+ template = query.sql_template
1730+ template = template.replace("%", "%%")
1731+ # template = template.replace("{", "{{").replace("}", "}}")
1732+ sql = template.format(
1733+ **dict([
1734+ (arg_name, "%s")
1735+ for arg_name in query.argument_list]))
1736+ # Construct argument list using defaults for missing values
1737+ sql_args = [
1738+ arguments.get(arg_name, self.lookup_argument(arg_name).default)
1739+ for arg_name in query.argument_list]
1740+ with contextlib.closing(connection.cursor()) as cursor:
1741+ # Execute the query with the specified arguments
1742+ cursor.execute(sql, sql_args)
1743+ # Get and return the results
1744+ rows = cursor.fetchall()
1745+ columns = cursor.description
1746+ return rows, columns
1747+
1748+
1749+class DataReport(RepositoryItem):
1750+ """
1751+ Data reports are small snippets of xml that define
1752+ a limited django template.
1753+ """
1754+
1755+ repository = DataReportRepository()
1756+
1757+ def __init__(self, **kwargs):
1758+ self._html = None
1759+ self._data = kwargs
1760+
1761+ def __unicode__(self):
1762+ return self.title
1763+
1764+ def __repr__(self):
1765+ return "<DataReport name=%r>" % (self.name,)
1766+
1767+ @models.permalink
1768+ def get_absolute_url(self):
1769+ return ("dashboard_app.views.report_detail", [self.name])
1770+
1771+ def _get_raw_html(self):
1772+ pathname = os.path.join(self.base_path, self.path)
1773+ try:
1774+ with open(pathname) as stream:
1775+ return stream.read()
1776+ except (IOError, OSError) as ex:
1777+ logging.error("Unable to load DataReport HTML file from %r: %s", pathname, ex)
1778+ return ""
1779+
1780+ def _get_html_template(self):
1781+ return Template(self._get_raw_html())
1782+
1783+ def _get_html_template_context(self):
1784+ return Context({
1785+ "API_URL": reverse("dashboard_app.views.dashboard_xml_rpc_handler"),
1786+ "STATIC_URL": settings.STATIC_URL
1787+ })
1788+
1789+ def get_html(self):
1790+ DEBUG = getattr(settings, "DEBUG", False)
1791+ if self._html is None or DEBUG is True:
1792+ template = self._get_html_template()
1793+ context = self._get_html_template_context()
1794+ self._html = template.render(context)
1795+ return self._html
1796+
1797+ @property
1798+ def title(self):
1799+ return self._data['title']
1800+
1801+ @property
1802+ def path(self):
1803+ return self._data['path']
1804+
1805+ @property
1806+ def name(self):
1807+ return self._data['name']
1808+
1809+ @property
1810+ def bug_report_url(self):
1811+ return self._data.get('bug_report_url')
1812+
1813+ @property
1814+ def author(self):
1815+ return self._data.get('author')
1816+
1817+ @property
1818+ def front_page(self):
1819+ return self._data['front_page']
1820+
1821+
1822+class Tag(models.Model):
1823+ """
1824+ Tag used for marking test runs.
1825+ """
1826+ name = models.SlugField(
1827+ verbose_name=_(u"Tag"),
1828+ max_length=256,
1829+ db_index=True,
1830+ unique=True)
1831+
1832+ def __unicode__(self):
1833+ return self.name
1834+
1835+
1836+class Image(models.Model):
1837+
1838+ name = models.SlugField(max_length=1024, unique=True)
1839+
1840+ filter = models.ForeignKey("TestRunFilter", related_name='+', null=True)
1841+
1842+ def __unicode__(self):
1843+ owner_name = getattr(self.filter, 'owner_name', '<NULL>')
1844+ return '%s, based on %s' % (self.name, owner_name)
1845+
1846+ @models.permalink
1847+ def get_absolute_url(self):
1848+ return ("dashboard_app.views.images.image_report_detail", (), dict(name=self.name))
1849+
1850+
1851+class ImageSet(models.Model):
1852+
1853+ name = models.CharField(max_length=1024, unique=True)
1854+
1855+ images = models.ManyToManyField(Image)
1856+
1857+ def __unicode__(self):
1858+ return self.name
1859+
1860+
1861+class LaunchpadBug(models.Model):
1862+
1863+ bug_id = models.PositiveIntegerField(unique=True)
1864+
1865+ test_runs = models.ManyToManyField(TestRun, related_name='launchpad_bugs')
1866+
1867+ def __unicode__(self):
1868+ return unicode(self.bug_id)
1869+
1870+@receiver(post_delete)
1871+def file_cleanup(sender, instance, **kwargs):
1872+ """
1873+ Signal receiver used for remove FieldFile attachments when removing
1874+ objects (Bundle and Attachment) from the database.
1875+ """
1876+ if instance is None or sender not in (Bundle, Attachment):
1877+ return
1878+ meta = sender._meta
1879+
1880+ for field_name in meta.get_all_field_names():
1881+
1882+ # object that represents the metadata of the field
1883+ try:
1884+ field_meta = meta.get_field(field_name)
1885+ except FieldDoesNotExist:
1886+ continue
1887+
1888+ # we just want the FileField's, not all the fields
1889+ if not isinstance(field_meta, models.FileField):
1890+ continue
1891+
1892+ # the field itself is a FieldFile instance, proxied by FileField
1893+ field = getattr(instance, field_name)
1894+
1895+ # the 'path' attribute contains the name of the file we need
1896+ if hasattr(field, 'path') and os.path.exists(field.path):
1897+ field.storage.delete(field.path)
1898+
1899+
1900+class TestRunFilterAttribute(models.Model):
1901+
1902+ name = models.CharField(max_length=1024)
1903+ value = models.CharField(max_length=1024)
1904+
1905+ filter = models.ForeignKey("TestRunFilter", related_name="attributes")
1906+
1907+ def __unicode__(self):
1908+ return '%s = %s' % (self.name, self.value)
1909+
1910+
1911+class TestRunFilterTest(models.Model):
1912+
1913+ test = models.ForeignKey(Test, related_name="+")
1914+ filter = models.ForeignKey("TestRunFilter", related_name="tests")
1915+ index = models.PositiveIntegerField(
1916+ help_text = _(u"The index of this test in the filter"))
1917+
1918+ def __unicode__(self):
1919+ return unicode(self.test)
1920+
1921+
1922+class TestRunFilterTestCase(models.Model):
1923+
1924+ test_case = models.ForeignKey(TestCase, related_name="+")
1925+ test = models.ForeignKey(TestRunFilterTest, related_name="cases")
1926+ index = models.PositiveIntegerField(
1927+ help_text = _(u"The index of this case in the test"))
1928+
1929+ def __unicode__(self):
1930+ return unicode(self.test_case)
1931+
1932+
1933+class TestRunFilter(models.Model):
1934+
1935+ owner = models.ForeignKey(User)
1936+
1937+ name = models.SlugField(
1938+ max_length=1024,
1939+ help_text=("The <b>name</b> of a filter is used to refer to it in "
1940+ "the web UI and in email notifications triggered by this "
1941+ "filter."))
1942+
1943+ @property
1944+ def owner_name(self):
1945+ return '~%s/%s' % (self.owner.username, self.name)
1946+
1947+ class Meta:
1948+ unique_together = (('owner', 'name'))
1949+
1950+ bundle_streams = models.ManyToManyField(BundleStream)
1951+ bundle_streams.help_text = 'A filter only matches tests within the given <b>bundle streams</b>.'
1952+
1953+ public = models.BooleanField(
1954+ default=False, help_text="Whether other users can see this filter.")
1955+
1956+ build_number_attribute = models.CharField(
1957+ max_length=1024, blank=True, null=True,
1958+ help_text="For some filters, there is a natural <b>build number</b>. If you specify the name of the attribute that contains the build number here, the results of the filter will be grouped and ordered by this build number.")
1959+
1960+ uploaded_by = models.ForeignKey(
1961+ User, null=True, blank=True, related_name='+',
1962+ help_text="Only consider bundles uploaded by this user")
1963+
1964+ def as_data(self):
1965+ tests = []
1966+ for trftest in self.tests.order_by('index').prefetch_related('cases'):
1967+ tests.append({
1968+ 'test': trftest.test,
1969+ 'test_cases': [trftestcase.test_case for trftestcase in trftest.cases.all().select_related('test_case')],
1970+ })
1971+ return {
1972+ 'bundle_streams': self.bundle_streams.all(),
1973+ 'attributes': self.attributes.all().values_list('name', 'value'),
1974+ 'tests': tests,
1975+ 'build_number_attribute': self.build_number_attribute,
1976+ 'uploaded_by': self.uploaded_by,
1977+ }
1978+
1979+ def __unicode__(self):
1980+ return "<TestRunFilter ~%s/%s>" % (self.owner.username, self.name)
1981+
1982+ # given bundle:
1983+ # select from filter
1984+ # where bundle.bundle_stream in filter.bundle_streams
1985+ # and filter.test in (select test from bundle.test_runs)
1986+ # and all the attributes on the filter are on a testrun in the bundle
1987+ # = the minimum over testrun (the number of attributes on the filter that are not on the testrun) is 0
1988+ # and (filter.test_case is null
1989+ # or filter.test_case in select test_case from bundle.test_runs.test_results.test_cases)
1990+
1991+ @classmethod
1992+ def matches_against_bundle(self, bundle):
1993+ from dashboard_app.filters import FilterMatch
1994+ bundle_filters = bundle.bundle_stream.testrunfilter_set.all()
1995+ attribute_filters = bundle_filters.extra(
1996+ where=[
1997+ """(select min((select count(*)
1998+ from dashboard_app_testrunfilterattribute
1999+ where filter_id = dashboard_app_testrunfilter.id
2000+ and (name, value) not in (select name, value
2001+ from dashboard_app_namedattribute
2002+ where content_type_id = (
2003+ select django_content_type.id from django_content_type
2004+ where app_label = 'dashboard_app' and model='testrun')
2005+ and object_id = dashboard_app_testrun.id)))
2006+ from dashboard_app_testrun where dashboard_app_testrun.bundle_id = %s) = 0""" % bundle.id],
2007+ )
2008+ no_test_filters = list(attribute_filters.annotate(models.Count('tests')).filter(tests__count=0))
2009+ attribute_filters = list(attribute_filters)
2010+ no_test_case_filters = list(
2011+ TestRunFilter.objects.filter(
2012+ id__in=TestRunFilterTest.objects.filter(
2013+ filter__in=attribute_filters, test__in=bundle.test_runs.all().values('test_id')).annotate(
2014+ models.Count('cases')).filter(cases__count=0).values('filter__id'),
2015+ ))
2016+ tcf = TestRunFilter.objects.filter(
2017+ id__in=TestRunFilterTest.objects.filter(
2018+ filter__in=attribute_filters,
2019+ cases__test_case__id__in=bundle.test_runs.all().values('test_results__test_case__id')
2020+ ).values('filter__id')
2021+ )
2022+ test_case_filters = list(tcf)
2023+
2024+ filters = set(test_case_filters + no_test_case_filters + no_test_filters)
2025+ matches = []
2026+ bundle_with_counts = Bundle.objects.annotate(
2027+ pass_count=models.Sum('test_runs__denormalization__count_pass'),
2028+ unknown_count=models.Sum('test_runs__denormalization__count_unknown'),
2029+ skip_count=models.Sum('test_runs__denormalization__count_skip'),
2030+ fail_count=models.Sum('test_runs__denormalization__count_fail')).get(
2031+ id=bundle.id)
2032+ for filter in filters:
2033+ match = FilterMatch()
2034+ match.filter = filter
2035+ match.filter_data = filter.as_data()
2036+ match.test_runs = list(bundle.test_runs.all())
2037+ match.specific_results = list(
2038+ TestResult.objects.filter(
2039+ test_case__id__in=filter.tests.all().values('cases__test_case__id'),
2040+ test_run__bundle=bundle))
2041+ b = bundle_with_counts
2042+ match.result_count = b.unknown_count + b.skip_count + b.pass_count + b.fail_count
2043+ match.pass_count = bundle_with_counts.pass_count
2044+ matches.append(match)
2045+ return matches
2046+
2047+ @models.permalink
2048+ def get_absolute_url(self):
2049+ return (
2050+ "dashboard_app.views.filters.views.filter_detail",
2051+ [self.owner.username, self.name])
2052+
2053+
2054+class TestRunFilterSubscription(models.Model):
2055+
2056+ user = models.ForeignKey(User)
2057+
2058+ filter = models.ForeignKey(TestRunFilter)
2059+
2060+ class Meta:
2061+ unique_together = (('user', 'filter'))
2062+
2063+ NOTIFICATION_FAILURE, NOTIFICATION_ALWAYS = range(2)
2064+
2065+ NOTIFICATION_CHOICES = (
2066+ (NOTIFICATION_FAILURE, "Only when failed"),
2067+ (NOTIFICATION_ALWAYS, "Always"))
2068+
2069+ level = models.IntegerField(
2070+ default=NOTIFICATION_FAILURE, choices=NOTIFICATION_CHOICES,
2071+ help_text=("You can choose to be <b>notified by email</b>:<ul><li>whenever a test "
2072+ "that matches the criteria of this filter is executed"
2073+ "</li><li>only when a test that matches the criteria of this filter fails</ul>"))
2074+
2075+ @classmethod
2076+ def recipients_for_bundle(cls, bundle):
2077+ matches = TestRunFilter.matches_against_bundle(bundle)
2078+ matches_by_filter_id = {}
2079+ for match in matches:
2080+ matches_by_filter_id[match.filter.id] = match
2081+ args = [models.Q(filter_id__in=list(matches_by_filter_id))]
2082+ bs = bundle.bundle_stream
2083+ if not bs.is_public:
2084+ if bs.group:
2085+ args.append(models.Q(user__in=bs.group.user_set.all()))
2086+ else:
2087+ args.append(models.Q(user=bs.user))
2088+ subscriptions = TestRunFilterSubscription.objects.filter(*args)
2089+ recipients = {}
2090+ for sub in subscriptions:
2091+ match = matches_by_filter_id[sub.filter.id]
2092+ if sub.level == cls.NOTIFICATION_FAILURE:
2093+ failure_found = False
2094+ if not match.filter_data['tests']:
2095+ failure_found = match.pass_count != match.result_count
2096+ else:
2097+ for t in match.filter_data['tests']:
2098+ if not t['test_cases']:
2099+ for tr in match.test_runs:
2100+ if tr.test == t.test:
2101+ if tr.denormalization.count_pass != tr.denormalization.count_all():
2102+ failure_found = True
2103+ break
2104+ if failure_found:
2105+ break
2106+ if not failure_found:
2107+ for r in match.specific_results:
2108+ if r.result != TestResult.RESULT_PASS:
2109+ failure_found = True
2110+ break
2111+ if not failure_found:
2112+ continue
2113+ recipients.setdefault(sub.user, []).append(match)
2114+ return recipients
2115+
2116+
2117+def send_bundle_notifications(sender, bundle, **kwargs):
2118+ try:
2119+ recipients = TestRunFilterSubscription.recipients_for_bundle(bundle)
2120+ domain = '???'
2121+ try:
2122+ site = Site.objects.get_current()
2123+ except (Site.DoesNotExist, ImproperlyConfigured):
2124+ pass
2125+ else:
2126+ domain = site.domain
2127+ url_prefix = 'http://%s' % domain
2128+ for user, matches in recipients.items():
2129+ logging.info("sending bundle notification to %s", user)
2130+ data = {'bundle': bundle, 'user': user, 'matches': matches, 'url_prefix': url_prefix}
2131+ mail = render_to_string(
2132+ 'dashboard_app/filter_subscription_mail.txt',
2133+ data)
2134+ filter_names = ', '.join(match.filter.name for match in matches)
2135+ send_mail(
2136+ "LAVA result notification: " + filter_names, mail,
2137+ settings.SERVER_EMAIL, [user.email])
2138+ except:
2139+ logging.exception("send_bundle_notifications failed")
2140+ raise
2141+
2142+
2143+bundle_was_deserialized.connect(send_bundle_notifications)
2144+
2145+
2146+class PMQABundleStream(models.Model):
2147+
2148+ bundle_stream = models.ForeignKey(BundleStream, related_name='+')
2149+
2150+
2151+class ImageReport(models.Model):
2152+
2153+ name = models.SlugField(max_length=1024, unique=True)
2154+
2155+ description = models.TextField(blank=True, null=True)
2156+
2157+ is_published = models.BooleanField(
2158+ default=False,
2159+ verbose_name='Published')
2160+
2161+ def __unicode__(self):
2162+ return self.name
2163+
2164+ @models.permalink
2165+ def get_absolute_url(self):
2166+ return ("dashboard_app.views.image_reports.views.image_report_display",
2167+ (), dict(name=self.name))
2168+
2169+# Chart types
2170+CHART_TYPES = ((r'pass/fail', 'Pass/Fail'),
2171+ (r'measurement', 'Measurement'))
2172+# Chart representation
2173+REPRESENTATION_TYPES = ((r'lines', 'Lines'),
2174+ (r'bars', 'Bars'))
2175+
2176+
2177+class ImageReportChart(models.Model):
2178+
2179+ class Meta:
2180+ unique_together = ("image_report", "name")
2181+
2182+ name = models.CharField(max_length=100)
2183+
2184+ description = models.TextField(blank=True, null=True)
2185+
2186+ image_report = models.ForeignKey(
2187+ ImageReport,
2188+ default=None,
2189+ null=False,
2190+ on_delete=models.CASCADE)
2191+
2192+ chart_type = models.CharField(
2193+ max_length=20,
2194+ choices=CHART_TYPES,
2195+ verbose_name='Chart type',
2196+ blank=False,
2197+ default="pass/fail",
2198+ )
2199+
2200+ target_goal = models.DecimalField(
2201+ blank = True,
2202+ decimal_places = 5,
2203+ max_digits = 10,
2204+ null = True,
2205+ verbose_name = 'Target goal')
2206+
2207+ is_interactive = models.BooleanField(
2208+ default=False,
2209+ verbose_name='Interactive')
2210+
2211+ is_data_table_visible = models.BooleanField(
2212+ default=False,
2213+ verbose_name='Data table visible')
2214+
2215+ def __unicode__(self):
2216+ return self.name
2217+
2218+ @models.permalink
2219+ def get_absolute_url(self):
2220+ return ("dashboard_app.views.image_reports.views.image_chart_detail",
2221+ (), dict(id=self.id))
2222+
2223+ def get_chart_data(self, user):
2224+ """
2225+ Pack data from filter to json format based on
2226+ selected tests/test cases.
2227+ """
2228+ from dashboard_app.filters import evaluate_filter
2229+
2230+ chart_data = self.get_basic_chart_data()
2231+ chart_data["filters"] = {}
2232+
2233+ for image_chart_filter in self.imagechartfilter_set.all():
2234+
2235+ chart_data["filters"][image_chart_filter.filter.id] = {
2236+ "owner": image_chart_filter.filter.owner.username,
2237+ "link": image_chart_filter.filter.get_absolute_url(),
2238+ "name": image_chart_filter.filter.name,
2239+ }
2240+
2241+ filter_data = image_chart_filter.filter.as_data()
2242+
2243+ if self.chart_type == "pass/fail":
2244+ # Prepare to filter the tests and test cases for the
2245+ # evaluate_filter call.
2246+ tests = []
2247+
2248+ for chart_test in image_chart_filter.imagecharttest_set.all():
2249+ tests.append({
2250+ 'test': chart_test.test,
2251+ 'test_cases': [],
2252+ })
2253+
2254+ filter_data['tests'] = tests
2255+ matches = evaluate_filter(user, filter_data, prefetch_related=['launchpad_bugs', 'test_results'])[:50]
2256+
2257+ for match in matches:
2258+ for test_run in match.test_runs:
2259+
2260+ denorm = test_run.denormalization
2261+ bug_ids = sorted(
2262+ [b.bug_id for b in test_run.launchpad_bugs.all()])
2263+
2264+ alias = ImageChartTest.objects.get(
2265+ image_chart_filter=image_chart_filter,
2266+ test=test_run.test).name
2267+
2268+ if not alias:
2269+ alias = "%s: %s" % (image_chart_filter.filter.name,
2270+ test_run.test.test_id)
2271+
2272+ chart_item = {
2273+ "filter_rep": image_chart_filter.representation,
2274+ "test_name": test_run.test.test_id,
2275+ "link": test_run.get_absolute_url(),
2276+ "alias": alias,
2277+ "number": str(match.tag),
2278+ "date": str(test_run.bundle.uploaded_on),
2279+ "pass": denorm.count_fail == 0,
2280+ "uuid": test_run.analyzer_assigned_uuid,
2281+ "passes": denorm.count_pass,
2282+ "total": denorm.count_pass + denorm.count_fail,
2283+ "bug_ids": bug_ids,
2284+ }
2285+
2286+ chart_data["test_data"][test_run.id] = chart_item
2287+
2288+ else:
2289+ # Prepare to filter the tests and test cases for the
2290+ # evaluate_filter call.
2291+ tests = []
2292+ test_cases = TestCase.objects.filter(imagecharttestcase__image_chart_filter__image_chart=self).distinct('id')
2293+ tests_all = Test.objects.filter(test_cases__in=test_cases).distinct('id').prefetch_related('test_cases')
2294+
2295+ for test in tests_all:
2296+ tests.append({
2297+ 'test': test,
2298+ 'test_cases': [test_case for test_case in test_cases if test_case in test.test_cases.all()],
2299+ })
2300+
2301+ filter_data['tests'] = tests
2302+ matches = evaluate_filter(user, filter_data)[:50]
2303+
2304+ for match in matches:
2305+ for test_result in match.specific_results:
2306+
2307+ alias = ImageChartTestCase.objects.get(
2308+ image_chart_filter=image_chart_filter,
2309+ test_case=test_result.test_case).name
2310+ if not alias:
2311+ alias = "%s: %s: %s" % (
2312+ image_chart_filter.filter.name,
2313+ test_result.test_run.test.test_id,
2314+ test_result.test_case.test_case_id
2315+ )
2316+
2317+ chart_item = {
2318+ "run_link": test_result.test_run.get_absolute_url(),
2319+ "filter_rep": image_chart_filter.representation,
2320+ "test_name": test_result.test_run.test.test_id,
2321+ "alias": alias,
2322+ "test_case_name": test_result.test_case.test_case_id,
2323+ "units": test_result.units,
2324+ "measurement": test_result.measurement,
2325+ "link": test_result.get_absolute_url(),
2326+ "pass": test_result.result == 0,
2327+ "number": str(match.tag),
2328+ "date": str(test_result.test_run.bundle.uploaded_on),
2329+ }
2330+ chart_data["test_data"][
2331+ test_result.id] = chart_item
2332+
2333+ return chart_data
2334+
2335+ def get_basic_chart_data(self):
2336+ chart_data = {}
2337+ fields = ["name", "chart_type", "description", "is_data_table_visible",
2338+ "is_interactive", "target_goal"]
2339+
2340+ for field in fields:
2341+ chart_data[field] = getattr(self, field)
2342+
2343+ chart_data["test_data"] = {}
2344+ return chart_data
2345+
2346+class ImageChartFilter(models.Model):
2347+
2348+ image_chart = models.ForeignKey(
2349+ ImageReportChart,
2350+ null=False,
2351+ on_delete=models.CASCADE)
2352+
2353+ filter = models.ForeignKey(
2354+ TestRunFilter,
2355+ null=True,
2356+ on_delete=models.SET_NULL)
2357+
2358+ representation = models.CharField(
2359+ max_length=20,
2360+ choices=REPRESENTATION_TYPES,
2361+ verbose_name='Representation',
2362+ blank=False,
2363+ default="lines",
2364+ )
2365+
2366+ @models.permalink
2367+ def get_absolute_url(self):
2368+ return (
2369+ "dashboard_app.views.image_reports.views.image_chart_filter_edit",
2370+ (), dict(id=self.id))
2371+
2372+
2373+class ImageChartTest(models.Model):
2374+
2375+ class Meta:
2376+ unique_together = ("image_chart_filter", "test")
2377+
2378+ image_chart_filter = models.ForeignKey(
2379+ ImageChartFilter,
2380+ null=False,
2381+ on_delete=models.CASCADE)
2382+
2383+ test = models.ForeignKey(
2384+ Test,
2385+ null=False,
2386+ on_delete=models.CASCADE)
2387+
2388+ name = models.CharField(max_length=200)
2389+
2390+
2391+class ImageChartTestCase(models.Model):
2392+
2393+ class Meta:
2394+ unique_together = ("image_chart_filter", "test_case")
2395+
2396+ image_chart_filter = models.ForeignKey(
2397+ ImageChartFilter,
2398+ null=False,
2399+ on_delete=models.CASCADE)
2400+
2401+ test_case = models.ForeignKey(
2402+ TestCase,
2403+ null=False,
2404+ on_delete=models.CASCADE)
2405+
2406+ name = models.CharField(max_length=200)
2407+
2408
2409=== added directory 'dashboard_app/static'
2410=== added directory 'dashboard_app/static/dashboard_app'
2411=== added directory 'dashboard_app/static/dashboard_app/css'
2412=== added file 'dashboard_app/static/dashboard_app/css/image-charts.css.OTHER'
2413--- dashboard_app/static/dashboard_app/css/image-charts.css.OTHER 1970-01-01 00:00:00 +0000
2414+++ dashboard_app/static/dashboard_app/css/image-charts.css.OTHER 2013-09-23 10:56:27 +0000
2415@@ -0,0 +1,138 @@
2416+@import url("../../admin/css/widgets.css");
2417+
2418+div.selector { clear: both; }
2419+div.selector span.helptext { display: none; }
2420+div.selector h2 { margin: 0; font-size: 11pt; }
2421+div.selector a { text-decoration: none; }
2422+div.selector select { height: 10em; }
2423+div.selector ul.selector-chooser { margin-top: 5.5em; }
2424+div.selector .selector-chosen select {
2425+ border: 1px solid rgb(204, 204, 204);
2426+ border-top: none;
2427+}
2428+
2429+.list-container {
2430+ border: 1px solid #000000;
2431+ clear: both;
2432+ margin: 10px 10px 10px 10px;
2433+ padding: 10px;
2434+ width: 50%;
2435+}
2436+
2437+.form-field {
2438+ margin-bottom: 5px;
2439+ vertical-align: top;
2440+}
2441+
2442+.form-field label {
2443+ vertical-align: top;
2444+ width: 100px;
2445+ display: inline-block;
2446+ margin-left: 10px;
2447+}
2448+
2449+.submit-button {
2450+ margin-top: 20px;
2451+ margin-left: 10px;
2452+}
2453+
2454+.filter-headline {
2455+ font-weight: bold;
2456+ font-size: 16px;
2457+}
2458+
2459+.filter-container {
2460+ margin-bottom: 10px;
2461+ clear: both;
2462+}
2463+
2464+.filter-title {
2465+ font-weight: bold;
2466+ font-size: 15px;
2467+ margin-bottom: 10px;
2468+}
2469+
2470+.chart-title {
2471+ font-weight: bold;
2472+ font-size: 15px;
2473+ margin-bottom: 10px;
2474+}
2475+
2476+.errors {
2477+ color: red;
2478+}
2479+
2480+.fields-container {
2481+ margin-left: 10px;
2482+}
2483+
2484+#filters_div {
2485+ margin: 10px 0 0 10px;
2486+ border: 1px solid #000000;
2487+ clear: both;
2488+ width: 75%;
2489+ padding: 5px 0 10px 10px;
2490+ overflow: auto;
2491+}
2492+
2493+#alias_container {
2494+ font-weight: bold;
2495+ float: left;
2496+ display: none;
2497+}
2498+
2499+.outer-chart {
2500+}
2501+
2502+.inner-chart {
2503+ height: 200px;
2504+ width: 82%;
2505+ display: inline-block;
2506+}
2507+
2508+.legend {
2509+ height: 200px;
2510+ width: 15%;
2511+ display: inline-block;
2512+}
2513+
2514+#main_container {
2515+ padding-left: 20px;
2516+ margin-bottom: 20px;
2517+}
2518+
2519+.headline-container {
2520+ margin: 20px 0 10px 0px;
2521+ width: 80%;
2522+}
2523+
2524+.dates-container {
2525+ margin: 10px 0 10px 0px;
2526+ width: 80%;
2527+}
2528+
2529+.filter-links-container {
2530+ margin: 20px 0 10px 0px;
2531+ width: 80%;
2532+}
2533+
2534+.filter-links-container img {
2535+ border: 0px;
2536+ float: right;
2537+ font-size: 12px;
2538+ margin-top: 3px;
2539+}
2540+
2541+.chart-headline {
2542+ font-size: 16px;
2543+ font-weight: bold;
2544+ color: #98c13d;
2545+ margin-right: 20px;
2546+}
2547+
2548+.sortable-placeholder {
2549+ border: 1px solid #000000;
2550+ background-color: #ecffc2;
2551+ height: 200px;
2552+ width: 80%;
2553+}
2554
2555=== added directory 'dashboard_app/static/dashboard_app/js'
2556=== added file 'dashboard_app/static/dashboard_app/js/excanvas.min.js.OTHER'
2557--- dashboard_app/static/dashboard_app/js/excanvas.min.js.OTHER 1970-01-01 00:00:00 +0000
2558+++ dashboard_app/static/dashboard_app/js/excanvas.min.js.OTHER 2013-09-23 10:56:27 +0000
2559@@ -0,0 +1,1 @@
2560+if(!document.createElement("canvas").getContext){(function(){var ab=Math;var n=ab.round;var l=ab.sin;var A=ab.cos;var H=ab.abs;var N=ab.sqrt;var d=10;var f=d/2;var z=+navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];function y(){return this.context_||(this.context_=new D(this))}var t=Array.prototype.slice;function g(j,m,p){var i=t.call(arguments,2);return function(){return j.apply(m,i.concat(t.call(arguments)))}}function af(i){return String(i).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}function Y(m,j,i){if(!m.namespaces[j]){m.namespaces.add(j,i,"#default#VML")}}function R(j){Y(j,"g_vml_","urn:schemas-microsoft-com:vml");Y(j,"g_o_","urn:schemas-microsoft-com:office:office");if(!j.styleSheets.ex_canvas_){var i=j.createStyleSheet();i.owningElement.id="ex_canvas_";i.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}R(document);var e={init:function(i){var j=i||document;j.createElement("canvas");j.attachEvent("onreadystatechange",g(this.init_,this,j))},init_:function(p){var m=p.getElementsByTagName("canvas");for(var j=0;j<m.length;j++){this.initElement(m[j])}},initElement:function(j){if(!j.getContext){j.getContext=y;R(j.ownerDocument);j.innerHTML="";j.attachEvent("onpropertychange",x);j.attachEvent("onresize",W);var i=j.attributes;if(i.width&&i.width.specified){j.style.width=i.width.nodeValue+"px"}else{j.width=j.clientWidth}if(i.height&&i.height.specified){j.style.height=i.height.nodeValue+"px"}else{j.height=j.clientHeight}}return j}};function x(j){var i=j.srcElement;switch(j.propertyName){case"width":i.getContext().clearRect();i.style.width=i.attributes.width.nodeValue+"px";i.firstChild.style.width=i.clientWidth+"px";break;case"height":i.getContext().clearRect();i.style.height=i.attributes.height.nodeValue+"px";i.firstChild.style.height=i.clientHeight+"px";break}}function W(j){var i=j.srcElement;if(i.firstChild){i.firstChild.style.width=i.clientWidth+"px";i.firstChild.style.height=i.clientHeight+"px"}}e.init();var k=[];for(var ae=0;ae<16;ae++){for(var ad=0;ad<16;ad++){k[ae*16+ad]=ae.toString(16)+ad.toString(16)}}function B(){return[[1,0,0],[0,1,0],[0,0,1]]}function J(p,m){var j=B();for(var i=0;i<3;i++){for(var ah=0;ah<3;ah++){var Z=0;for(var ag=0;ag<3;ag++){Z+=p[i][ag]*m[ag][ah]}j[i][ah]=Z}}return j}function v(j,i){i.fillStyle=j.fillStyle;i.lineCap=j.lineCap;i.lineJoin=j.lineJoin;i.lineWidth=j.lineWidth;i.miterLimit=j.miterLimit;i.shadowBlur=j.shadowBlur;i.shadowColor=j.shadowColor;i.shadowOffsetX=j.shadowOffsetX;i.shadowOffsetY=j.shadowOffsetY;i.strokeStyle=j.strokeStyle;i.globalAlpha=j.globalAlpha;i.font=j.font;i.textAlign=j.textAlign;i.textBaseline=j.textBaseline;i.arcScaleX_=j.arcScaleX_;i.arcScaleY_=j.arcScaleY_;i.lineScale_=j.lineScale_}var b={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"};function M(j){var p=j.indexOf("(",3);var i=j.indexOf(")",p+1);var m=j.substring(p+1,i).split(",");if(m.length!=4||j.charAt(3)!="a"){m[3]=1}return m}function c(i){return parseFloat(i)/100}function r(j,m,i){return Math.min(i,Math.max(m,j))}function I(ag){var i,ai,aj,ah,ak,Z;ah=parseFloat(ag[0])/360%360;if(ah<0){ah++}ak=r(c(ag[1]),0,1);Z=r(c(ag[2]),0,1);if(ak==0){i=ai=aj=Z}else{var j=Z<0.5?Z*(1+ak):Z+ak-Z*ak;var m=2*Z-j;i=a(m,j,ah+1/3);ai=a(m,j,ah);aj=a(m,j,ah-1/3)}return"#"+k[Math.floor(i*255)]+k[Math.floor(ai*255)]+k[Math.floor(aj*255)]}function a(j,i,m){if(m<0){m++}if(m>1){m--}if(6*m<1){return j+(i-j)*6*m}else{if(2*m<1){return i}else{if(3*m<2){return j+(i-j)*(2/3-m)*6}else{return j}}}}var C={};function F(j){if(j in C){return C[j]}var ag,Z=1;j=String(j);if(j.charAt(0)=="#"){ag=j}else{if(/^rgb/.test(j)){var p=M(j);var ag="#",ah;for(var m=0;m<3;m++){if(p[m].indexOf("%")!=-1){ah=Math.floor(c(p[m])*255)}else{ah=+p[m]}ag+=k[r(ah,0,255)]}Z=+p[3]}else{if(/^hsl/.test(j)){var p=M(j);ag=I(p);Z=p[3]}else{ag=b[j]||j}}}return C[j]={color:ag,alpha:Z}}var o={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var L={};function E(i){if(L[i]){return L[i]}var p=document.createElement("div");var m=p.style;try{m.font=i}catch(j){}return L[i]={style:m.fontStyle||o.style,variant:m.fontVariant||o.variant,weight:m.fontWeight||o.weight,size:m.fontSize||o.size,family:m.fontFamily||o.family}}function u(m,j){var i={};for(var ah in m){i[ah]=m[ah]}var ag=parseFloat(j.currentStyle.fontSize),Z=parseFloat(m.size);if(typeof m.size=="number"){i.size=m.size}else{if(m.size.indexOf("px")!=-1){i.size=Z}else{if(m.size.indexOf("em")!=-1){i.size=ag*Z}else{if(m.size.indexOf("%")!=-1){i.size=(ag/100)*Z}else{if(m.size.indexOf("pt")!=-1){i.size=Z/0.75}else{i.size=ag}}}}}i.size*=0.981;return i}function ac(i){return i.style+" "+i.variant+" "+i.weight+" "+i.size+"px "+i.family}var s={butt:"flat",round:"round"};function S(i){return s[i]||"square"}function D(i){this.m_=B();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=d*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var m="width:"+i.clientWidth+"px;height:"+i.clientHeight+"px;overflow:hidden;position:absolute";var j=i.ownerDocument.createElement("div");j.style.cssText=m;i.appendChild(j);var p=j.cloneNode(false);p.style.backgroundColor="red";p.style.filter="alpha(opacity=0)";i.appendChild(p);this.element_=j;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var q=D.prototype;q.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};q.beginPath=function(){this.currentPath_=[]};q.moveTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"moveTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.lineTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"lineTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.bezierCurveTo=function(m,j,ak,aj,ai,ag){var i=V(this,ai,ag);var ah=V(this,m,j);var Z=V(this,ak,aj);K(this,ah,Z,i)};function K(i,Z,m,j){i.currentPath_.push({type:"bezierCurveTo",cp1x:Z.x,cp1y:Z.y,cp2x:m.x,cp2y:m.y,x:j.x,y:j.y});i.currentX_=j.x;i.currentY_=j.y}q.quadraticCurveTo=function(ai,m,j,i){var ah=V(this,ai,m);var ag=V(this,j,i);var aj={x:this.currentX_+2/3*(ah.x-this.currentX_),y:this.currentY_+2/3*(ah.y-this.currentY_)};var Z={x:aj.x+(ag.x-this.currentX_)/3,y:aj.y+(ag.y-this.currentY_)/3};K(this,aj,Z,ag)};q.arc=function(al,aj,ak,ag,j,m){ak*=d;var ap=m?"at":"wa";var am=al+A(ag)*ak-f;var ao=aj+l(ag)*ak-f;var i=al+A(j)*ak-f;var an=aj+l(j)*ak-f;if(am==i&&!m){am+=0.125}var Z=V(this,al,aj);var ai=V(this,am,ao);var ah=V(this,i,an);this.currentPath_.push({type:ap,x:Z.x,y:Z.y,radius:ak,xStart:ai.x,yStart:ai.y,xEnd:ah.x,yEnd:ah.y})};q.rect=function(m,j,i,p){this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath()};q.strokeRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.stroke();this.currentPath_=Z};q.fillRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.fill();this.currentPath_=Z};q.createLinearGradient=function(j,p,i,m){var Z=new U("gradient");Z.x0_=j;Z.y0_=p;Z.x1_=i;Z.y1_=m;return Z};q.createRadialGradient=function(p,ag,m,j,Z,i){var ah=new U("gradientradial");ah.x0_=p;ah.y0_=ag;ah.r0_=m;ah.x1_=j;ah.y1_=Z;ah.r1_=i;return ah};q.drawImage=function(aq,m){var aj,ah,al,ay,ao,am,at,aA;var ak=aq.runtimeStyle.width;var ap=aq.runtimeStyle.height;aq.runtimeStyle.width="auto";aq.runtimeStyle.height="auto";var ai=aq.width;var aw=aq.height;aq.runtimeStyle.width=ak;aq.runtimeStyle.height=ap;if(arguments.length==3){aj=arguments[1];ah=arguments[2];ao=am=0;at=al=ai;aA=ay=aw}else{if(arguments.length==5){aj=arguments[1];ah=arguments[2];al=arguments[3];ay=arguments[4];ao=am=0;at=ai;aA=aw}else{if(arguments.length==9){ao=arguments[1];am=arguments[2];at=arguments[3];aA=arguments[4];aj=arguments[5];ah=arguments[6];al=arguments[7];ay=arguments[8]}else{throw Error("Invalid number of arguments")}}}var az=V(this,aj,ah);var p=at/2;var j=aA/2;var ax=[];var i=10;var ag=10;ax.push(" <g_vml_:group",' coordsize="',d*i,",",d*ag,'"',' coordorigin="0,0"',' style="width:',i,"px;height:",ag,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]||this.m_[1][1]!=1||this.m_[1][0]){var Z=[];Z.push("M11=",this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",n(az.x/d),",","Dy=",n(az.y/d),"");var av=az;var au=V(this,aj+al,ah);var ar=V(this,aj,ah+ay);var an=V(this,aj+al,ah+ay);av.x=ab.max(av.x,au.x,ar.x,an.x);av.y=ab.max(av.y,au.y,ar.y,an.y);ax.push("padding:0 ",n(av.x/d),"px ",n(av.y/d),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",Z.join(""),", sizingmethod='clip');")}else{ax.push("top:",n(az.y/d),"px;left:",n(az.x/d),"px;")}ax.push(' ">','<g_vml_:image src="',aq.src,'"',' style="width:',d*al,"px;"," height:",d*ay,'px"',' cropleft="',ao/ai,'"',' croptop="',am/aw,'"',' cropright="',(ai-ao-at)/ai,'"',' cropbottom="',(aw-am-aA)/aw,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",ax.join(""))};q.stroke=function(ao){var Z=10;var ap=10;var ag=5000;var ai={x:null,y:null};var an={x:null,y:null};for(var aj=0;aj<this.currentPath_.length;aj+=ag){var am=[];var ah=false;am.push("<g_vml_:shape",' filled="',!!ao,'"',' style="position:absolute;width:',Z,"px;height:",ap,'px;"',' coordorigin="0,0"',' coordsize="',d*Z,",",d*ap,'"',' stroked="',!ao,'"',' path="');var aq=false;for(var ak=aj;ak<Math.min(aj+ag,this.currentPath_.length);ak++){if(ak%ag==0&&ak>0){am.push(" m ",n(this.currentPath_[ak-1].x),",",n(this.currentPath_[ak-1].y))}var m=this.currentPath_[ak];var al;switch(m.type){case"moveTo":al=m;am.push(" m ",n(m.x),",",n(m.y));break;case"lineTo":am.push(" l ",n(m.x),",",n(m.y));break;case"close":am.push(" x ");m=null;break;case"bezierCurveTo":am.push(" c ",n(m.cp1x),",",n(m.cp1y),",",n(m.cp2x),",",n(m.cp2y),",",n(m.x),",",n(m.y));break;case"at":case"wa":am.push(" ",m.type," ",n(m.x-this.arcScaleX_*m.radius),",",n(m.y-this.arcScaleY_*m.radius)," ",n(m.x+this.arcScaleX_*m.radius),",",n(m.y+this.arcScaleY_*m.radius)," ",n(m.xStart),",",n(m.yStart)," ",n(m.xEnd),",",n(m.yEnd));break}if(m){if(ai.x==null||m.x<ai.x){ai.x=m.x}if(an.x==null||m.x>an.x){an.x=m.x}if(ai.y==null||m.y<ai.y){ai.y=m.y}if(an.y==null||m.y>an.y){an.y=m.y}}}am.push(' ">');if(!ao){w(this,am)}else{G(this,am,ai,an)}am.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",am.join(""))}};function w(m,ag){var j=F(m.strokeStyle);var p=j.color;var Z=j.alpha*m.globalAlpha;var i=m.lineScale_*m.lineWidth;if(i<1){Z*=i}ag.push("<g_vml_:stroke",' opacity="',Z,'"',' joinstyle="',m.lineJoin,'"',' miterlimit="',m.miterLimit,'"',' endcap="',S(m.lineCap),'"',' weight="',i,'px"',' color="',p,'" />')}function G(aq,ai,aK,ar){var aj=aq.fillStyle;var aB=aq.arcScaleX_;var aA=aq.arcScaleY_;var j=ar.x-aK.x;var p=ar.y-aK.y;if(aj instanceof U){var an=0;var aF={x:0,y:0};var ax=0;var am=1;if(aj.type_=="gradient"){var al=aj.x0_/aB;var m=aj.y0_/aA;var ak=aj.x1_/aB;var aM=aj.y1_/aA;var aJ=V(aq,al,m);var aI=V(aq,ak,aM);var ag=aI.x-aJ.x;var Z=aI.y-aJ.y;an=Math.atan2(ag,Z)*180/Math.PI;if(an<0){an+=360}if(an<0.000001){an=0}}else{var aJ=V(aq,aj.x0_,aj.y0_);aF={x:(aJ.x-aK.x)/j,y:(aJ.y-aK.y)/p};j/=aB*d;p/=aA*d;var aD=ab.max(j,p);ax=2*aj.r0_/aD;am=2*aj.r1_/aD-ax}var av=aj.colors_;av.sort(function(aN,i){return aN.offset-i.offset});var ap=av.length;var au=av[0].color;var at=av[ap-1].color;var az=av[0].alpha*aq.globalAlpha;var ay=av[ap-1].alpha*aq.globalAlpha;var aE=[];for(var aH=0;aH<ap;aH++){var ao=av[aH];aE.push(ao.offset*am+ax+" "+ao.color)}ai.push('<g_vml_:fill type="',aj.type_,'"',' method="none" focus="100%"',' color="',au,'"',' color2="',at,'"',' colors="',aE.join(","),'"',' opacity="',ay,'"',' g_o_:opacity2="',az,'"',' angle="',an,'"',' focusposition="',aF.x,",",aF.y,'" />')}else{if(aj instanceof T){if(j&&p){var ah=-aK.x;var aC=-aK.y;ai.push("<g_vml_:fill",' position="',ah/j*aB*aB,",",aC/p*aA*aA,'"',' type="tile"',' src="',aj.src_,'" />')}}else{var aL=F(aq.fillStyle);var aw=aL.color;var aG=aL.alpha*aq.globalAlpha;ai.push('<g_vml_:fill color="',aw,'" opacity="',aG,'" />')}}}q.fill=function(){this.stroke(true)};q.closePath=function(){this.currentPath_.push({type:"close"})};function V(j,Z,p){var i=j.m_;return{x:d*(Z*i[0][0]+p*i[1][0]+i[2][0])-f,y:d*(Z*i[0][1]+p*i[1][1]+i[2][1])-f}}q.save=function(){var i={};v(this,i);this.aStack_.push(i);this.mStack_.push(this.m_);this.m_=J(B(),this.m_)};q.restore=function(){if(this.aStack_.length){v(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function h(i){return isFinite(i[0][0])&&isFinite(i[0][1])&&isFinite(i[1][0])&&isFinite(i[1][1])&&isFinite(i[2][0])&&isFinite(i[2][1])}function aa(j,i,p){if(!h(i)){return}j.m_=i;if(p){var Z=i[0][0]*i[1][1]-i[0][1]*i[1][0];j.lineScale_=N(H(Z))}}q.translate=function(m,j){var i=[[1,0,0],[0,1,0],[m,j,1]];aa(this,J(i,this.m_),false)};q.rotate=function(j){var p=A(j);var m=l(j);var i=[[p,m,0],[-m,p,0],[0,0,1]];aa(this,J(i,this.m_),false)};q.scale=function(m,j){this.arcScaleX_*=m;this.arcScaleY_*=j;var i=[[m,0,0],[0,j,0],[0,0,1]];aa(this,J(i,this.m_),true)};q.transform=function(Z,p,ah,ag,j,i){var m=[[Z,p,0],[ah,ag,0],[j,i,1]];aa(this,J(m,this.m_),true)};q.setTransform=function(ag,Z,ai,ah,p,j){var i=[[ag,Z,0],[ai,ah,0],[p,j,1]];aa(this,i,true)};q.drawText_=function(am,ak,aj,ap,ai){var ao=this.m_,at=1000,j=0,ar=at,ah={x:0,y:0},ag=[];var i=u(E(this.font),this.element_);var p=ac(i);var au=this.element_.currentStyle;var Z=this.textAlign.toLowerCase();switch(Z){case"left":case"center":case"right":break;case"end":Z=au.direction=="ltr"?"right":"left";break;case"start":Z=au.direction=="rtl"?"right":"left";break;default:Z="left"}switch(this.textBaseline){case"hanging":case"top":ah.y=i.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":ah.y=-i.size/2.25;break}switch(Z){case"right":j=at;ar=0.05;break;case"center":j=ar=at/2;break}var aq=V(this,ak+ah.x,aj+ah.y);ag.push('<g_vml_:line from="',-j,' 0" to="',ar,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!ai,'" stroked="',!!ai,'" style="position:absolute;width:1px;height:1px;">');if(ai){w(this,ag)}else{G(this,ag,{x:-j,y:0},{x:ar,y:i.size})}var an=ao[0][0].toFixed(3)+","+ao[1][0].toFixed(3)+","+ao[0][1].toFixed(3)+","+ao[1][1].toFixed(3)+",0,0";var al=n(aq.x/d)+","+n(aq.y/d);ag.push('<g_vml_:skew on="t" matrix="',an,'" ',' offset="',al,'" origin="',j,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',af(am),'" style="v-text-align:',Z,";font:",af(p),'" /></g_vml_:line>');this.element_.insertAdjacentHTML("beforeEnd",ag.join(""))};q.fillText=function(m,i,p,j){this.drawText_(m,i,p,j,false)};q.strokeText=function(m,i,p,j){this.drawText_(m,i,p,j,true)};q.measureText=function(m){if(!this.textMeasureEl_){var i='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';this.element_.insertAdjacentHTML("beforeEnd",i);this.textMeasureEl_=this.element_.lastChild}var j=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(j.createTextNode(m));return{width:this.textMeasureEl_.offsetWidth}};q.clip=function(){};q.arcTo=function(){};q.createPattern=function(j,i){return new T(j,i)};function U(i){this.type_=i;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}U.prototype.addColorStop=function(j,i){i=F(i);this.colors_.push({offset:j,color:i.color,alpha:i.alpha})};function T(j,i){Q(j);switch(i){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=i;break;default:O("SYNTAX_ERR")}this.src_=j.src;this.width_=j.width;this.height_=j.height}function O(i){throw new P(i)}function Q(i){if(!i||i.nodeType!=1||i.tagName!="IMG"){O("TYPE_MISMATCH_ERR")}if(i.readyState!="complete"){O("INVALID_STATE_ERR")}}function P(i){this.code=this[i];this.message=i+": DOM Exception "+this.code}var X=P.prototype=new Error;X.INDEX_SIZE_ERR=1;X.DOMSTRING_SIZE_ERR=2;X.HIERARCHY_REQUEST_ERR=3;X.WRONG_DOCUMENT_ERR=4;X.INVALID_CHARACTER_ERR=5;X.NO_DATA_ALLOWED_ERR=6;X.NO_MODIFICATION_ALLOWED_ERR=7;X.NOT_FOUND_ERR=8;X.NOT_SUPPORTED_ERR=9;X.INUSE_ATTRIBUTE_ERR=10;X.INVALID_STATE_ERR=11;X.SYNTAX_ERR=12;X.INVALID_MODIFICATION_ERR=13;X.NAMESPACE_ERR=14;X.INVALID_ACCESS_ERR=15;X.VALIDATION_ERR=16;X.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=e;CanvasRenderingContext2D=D;CanvasGradient=U;CanvasPattern=T;DOMException=P})()};
2561\ No newline at end of file
2562
2563=== added file 'dashboard_app/static/dashboard_app/js/image-chart.js'
2564--- dashboard_app/static/dashboard_app/js/image-chart.js 1970-01-01 00:00:00 +0000
2565+++ dashboard_app/static/dashboard_app/js/image-chart.js 2013-09-23 10:56:27 +0000
2566@@ -0,0 +1,291 @@
2567+$(document).ready(function () {
2568+
2569+ // Add charts.
2570+ for (chart_id in chart_data) {
2571+ add_chart(chart_id, chart_data[chart_id]);
2572+ }
2573+
2574+ setup_sortable();
2575+});
2576+
2577+
2578+add_chart = function(chart_id, chart_data) {
2579+
2580+ if (chart_data.test_data) {
2581+ // Add chart container.
2582+ $("#main_container").append(
2583+ '<div id="chart_container_'+ chart_id + '"></div>');
2584+ // Add headline container.
2585+ $("#chart_container_" + chart_id).append(
2586+ '<div class="headline-container" id="headline_container_' +
2587+ chart_id + '"></div>');
2588+ // Add filter links used.
2589+ $("#chart_container_" + chart_id).append(
2590+ '<div class="filter-links-container" id="filter_links_container_' +
2591+ chart_id + '"></div>');
2592+ // Add dates/build numbers container.
2593+ $("#chart_container_" + chart_id).append(
2594+ '<div class="dates-container" id="dates_container_' +
2595+ chart_id + '"></div>');
2596+ // Add outer plot container.
2597+ $("#chart_container_" + chart_id).append(
2598+ '<div class="outer-chart" id="outer_container_' +
2599+ chart_id + '"></div>');
2600+ // Add inner plot container.
2601+ $("#outer_container_" + chart_id).append(
2602+ '<div class="inner-chart" id="inner_container_' +
2603+ chart_id + '"></div>');
2604+ // Add legend container.
2605+ $("#outer_container_" + chart_id).append(
2606+ '<div class="legend" id="legend_container_' +
2607+ chart_id + '"></div>');
2608+
2609+ // Add headline and description.
2610+ update_headline(chart_id, chart_data);
2611+ // Add dates/build numbers.
2612+ update_dates(chart_id, chart_data);
2613+ // Add filter links.
2614+ update_filter_links(chart_id, chart_data);
2615+ // Generate chart.
2616+ update_plot(chart_id, chart_data);
2617+ // Add source for saving charts as images.
2618+ update_img(chart_id);
2619+ // Update events.
2620+ if (chart_data["is_interactive"]) {
2621+ update_events(chart_id);
2622+ }
2623+ }
2624+}
2625+
2626+setup_sortable = function() {
2627+ // Set up sortable plugin.
2628+ $("#main_container").sortable({
2629+ axis: "y",
2630+ cursor: "move",
2631+ placeholder: "sortable-placeholder",
2632+ scroll: true,
2633+ scrollSensitivity: 50,
2634+ tolerance: "pointer",
2635+ });
2636+ $("#main_container").disableSelection();
2637+}
2638+
2639+update_events = function(chart_id) {
2640+ // Bind plotclick event.
2641+ $("#inner_container_"+chart_id).bind(
2642+ "plotclick",
2643+ function (event, pos, item) {
2644+ if (item) {
2645+ url = window.location.protocol + "//" +
2646+ window.location.host +
2647+ item.series.meta[item.dataIndex]["link"];
2648+ window.open(url, "_blank");
2649+ }
2650+ });
2651+
2652+ $("#inner_container_"+chart_id).bind(
2653+ "plothover",
2654+ function (event, pos, item) {
2655+ $("#tooltip").remove();
2656+ if (item) {
2657+ tooltip = item.series.meta[item.dataIndex]["tooltip"];
2658+ showTooltip(item.pageX, item.pageY, tooltip,
2659+ passes==total);
2660+ }
2661+ });
2662+}
2663+
2664+showTooltip = function(x, y, contents, pass) {
2665+ bkg_color = (pass)? '#98c13d' : '#ff7f7f';
2666+ $('<div id="tooltip">' + contents + '</div>').css({
2667+ position: 'absolute', display: 'none', top: y + 5, left: x + 5,
2668+ border: '1px solid #000', padding: '2px',
2669+ 'background-color': bkg_color, opacity: 0.80
2670+ }).appendTo("body").fadeIn(200);
2671+}
2672+
2673+update_headline = function(chart_id, chart_data) {
2674+ $("#headline_container_" + chart_id).append(
2675+ '<span class="chart-headline">' + chart_data["name"] + '</span>');
2676+ $("#headline_container_" + chart_id).append(
2677+ '<span>' + chart_data["description"] + '</span>');
2678+}
2679+
2680+update_filter_links = function(chart_id, chart_data) {
2681+
2682+ $("#filter_links_container_" + chart_id).append(
2683+ '<span style="margin-left: 30px;">Filters used:&nbsp;&nbsp;</span>');
2684+ filter_links = [];
2685+ for (filter_id in chart_data.filters) {
2686+ filter_links.push('<a href="' +
2687+ chart_data.filters[filter_id]["link"] + '">~' +
2688+ chart_data.filters[filter_id]["owner"] + '/' +
2689+ chart_data.filters[filter_id]["name"] +
2690+ '</a>');
2691+ }
2692+ filter_html = filter_links.join(", ");
2693+ $("#filter_links_container_" + chart_id).append(
2694+ '<span>' + filter_html + '</span>');
2695+
2696+ $("#filter_links_container_" + chart_id).append(
2697+ '<span class="chart-save-img">' +
2698+ '<a target="_blank" href=# id="chart_img_' + chart_id + '"><img' +
2699+ ' alt="Click to view as image"></a>' +
2700+ '</span>');
2701+}
2702+
2703+update_dates = function(chart_id, chart_data) {
2704+ $("#dates_container_" + chart_id).append(
2705+ '<span style="margin-left: 30px;">Start build number:&nbsp;&nbsp;</span>');
2706+ $("#dates_container_" + chart_id).append(
2707+ '<span><select><option value="2013-09-02">2013-09-02</option></select></span>');
2708+ $("#dates_container_" + chart_id).append(
2709+ '<span>&nbsp;&nbsp;&nbsp;&nbsp;End build number:&nbsp;&nbsp;</span>');
2710+ $("#dates_container_" + chart_id).append(
2711+ '<span><select><option value="2013-09-02">2013-09-02</option></select></span>');
2712+ $("#dates_container_" + chart_id).append(
2713+ '<span style="float: right;"><a href="#">Subscribe to target goal</></span>');
2714+
2715+}
2716+
2717+update_img = function(chart_id) {
2718+ canvas = $("#inner_container_" + chart_id + " > .flot-base").get(0);
2719+ var dataURL = canvas.toDataURL();
2720+ document.getElementById("chart_img_" + chart_id).href = dataURL;
2721+}
2722+
2723+update_plot = function(chart_id, chart_data) {
2724+
2725+ // Get the plot data.
2726+ plot_data = {};
2727+
2728+ // Maximum number of test runs.
2729+ max_iter = 0;
2730+
2731+ for (test_id in chart_data.test_data) {
2732+ // TODO: alias can't be the key in this array,
2733+ // it's not unique accross multiple or the same filters.
2734+ // Ensure that aliases are unique per chart.
2735+ row = chart_data.test_data[test_id];
2736+ if (!(row["alias"] in plot_data)) {
2737+ plot_data[row["alias"]] = {};
2738+ plot_data[row["alias"]]["representation"] = row["filter_rep"];
2739+ plot_data[row["alias"]]["data"] = [];
2740+ plot_data[row["alias"]]["meta"] = [];
2741+ }
2742+
2743+ // Current iterator for plot_data[test_alias][data].
2744+ iter = plot_data[row["alias"]]["data"].length;
2745+
2746+ if (chart_data["chart_type"] == "pass/fail") {
2747+ value = row["passes"];
2748+ tooltip = "Pass: " + value + ", Total: " + row["passes"];
2749+
2750+ } else {
2751+ value = row["measurement"];
2752+ tooltip = "Value: " + value;
2753+ }
2754+ plot_data[row["alias"]]["data"].push([iter, value]);
2755+ plot_data[row["alias"]]["meta"].push({
2756+ "link": row["link"],
2757+ "tooltip": tooltip,
2758+ });
2759+
2760+ if (iter > max_iter) {
2761+ max_iter = iter;
2762+ }
2763+ }
2764+
2765+ data = [];
2766+
2767+ // Prepare data and additional drawing options in series.
2768+ for (label in plot_data) {
2769+ if (plot_data[label]["representation"] == "bars") {
2770+ bars_options = {show: true};
2771+ lines_options = {show: false};
2772+ } else {
2773+ bars_options = {show: false};
2774+ lines_options = {show: true};
2775+ }
2776+
2777+ data.push({
2778+ label: label,
2779+ data: plot_data[label]["data"],
2780+ meta: plot_data[label]["meta"],
2781+ bars: bars_options,
2782+ lines: lines_options,
2783+ });
2784+ }
2785+
2786+ // Add target goal dashed line to the plot.
2787+ if (chart_data["target_goal"]) {
2788+ goal_data = [];
2789+ for (iter = 0; iter <= max_iter; iter++) {
2790+ goal_data.push([iter, chart_data["target_goal"]]);
2791+ }
2792+
2793+ data.push({data: goal_data, dashes: {show: true}, lines: {show: false}, color: "#000000"});
2794+ }
2795+
2796+ // Get all build numbers to be used as tick labels.
2797+ build_numbers = [];
2798+ for (test_id in chart_data.test_data) {
2799+
2800+ row = chart_data.test_data[test_id];
2801+
2802+ build_number = row["number"].split(' ')[0];
2803+ if (!isNumeric(build_number)) {
2804+ build_number = format_date(build_number);
2805+ }
2806+ build_numbers.push(build_number);
2807+ }
2808+
2809+ chart_width = $("#inner_container_" + chart_id).width();
2810+ var options = {
2811+ series: {
2812+ lines: { show: true },
2813+ points: { show: false },
2814+ bars: { barWidth: 0.5 },
2815+ },
2816+ grid: {
2817+ hoverable: true,
2818+ clickable: true,
2819+ },
2820+ legend: {
2821+ show: true,
2822+ position: "nw",
2823+// margin: [chart_width-40, 0],
2824+ container: "#legend_container_" + chart_id,
2825+ labelFormatter: function(label, series) {
2826+ if (label.length > 25) {
2827+ return label.substring(0,24) + "...";
2828+ }
2829+ return label;
2830+ },
2831+ },
2832+ xaxis: {
2833+ tickDecimals: 0,
2834+ tickFormatter: function (val, axis) {
2835+ return build_numbers[val];
2836+ },
2837+ },
2838+ yaxis: {
2839+ tickDecimals: 0,
2840+ labelWidth: 25,
2841+ },
2842+ canvas: true,
2843+ };
2844+
2845+ $.plot($("#outer_container_" + chart_id + " #inner_container_" + chart_id),
2846+ data, options);
2847+}
2848+
2849+isNumeric = function(n) {
2850+ return !isNaN(parseFloat(n)) && isFinite(n);
2851+}
2852+
2853+format_date = function(date_string) {
2854+ date = $.datepicker.parseDate("yy-mm-dd", date_string);
2855+ date_string = $.datepicker.formatDate("M d, yy", date);
2856+ return date_string;
2857+}
2858
2859=== added file 'dashboard_app/static/dashboard_app/js/jquery.flot.canvas.min.js'
2860--- dashboard_app/static/dashboard_app/js/jquery.flot.canvas.min.js 1970-01-01 00:00:00 +0000
2861+++ dashboard_app/static/dashboard_app/js/jquery.flot.canvas.min.js 2013-09-23 10:56:27 +0000
2862@@ -0,0 +1,28 @@
2863+/* Flot plugin for drawing all elements of a plot on the canvas.
2864+
2865+Copyright (c) 2007-2013 IOLA and Ole Laursen.
2866+Licensed under the MIT license.
2867+
2868+Flot normally produces certain elements, like axis labels and the legend, using
2869+HTML elements. This permits greater interactivity and customization, and often
2870+looks better, due to cross-browser canvas text inconsistencies and limitations.
2871+
2872+It can also be desirable to render the plot entirely in canvas, particularly
2873+if the goal is to save it as an image, or if Flot is being used in a context
2874+where the HTML DOM does not exist, as is the case within Node.js. This plugin
2875+switches out Flot's standard drawing operations for canvas-only replacements.
2876+
2877+Currently the plugin supports only axis labels, but it will eventually allow
2878+every element of the plot to be rendered directly to canvas.
2879+
2880+The plugin supports these options:
2881+
2882+{
2883+ canvas: boolean
2884+}
2885+
2886+The "canvas" option controls whether full canvas drawing is enabled, making it
2887+possible to toggle on and off. This is useful when a plot uses HTML text in the
2888+browser, but needs to redraw with canvas text when exporting as an image.
2889+
2890+*/(function(e){function o(t,o){var u=o.Canvas;n==null&&(r=u.prototype.getTextInfo,i=u.prototype.addText,n=u.prototype.render),u.prototype.render=function(){if(!t.getOptions().canvas)return n.call(this);var e=this.context,r=this._textCache;e.save(),e.textBaseline="middle";for(var i in r)if(s.call(r,i)){var o=r[i];for(var u in o)if(s.call(o,u)){var a=o[u],f=!0;for(var l in a)if(s.call(a,l)){var c=a[l],h=c.positions,p=c.lines;f&&(e.fillStyle=c.font.color,e.font=c.font.definition,f=!1);for(var d=0,v;v=h[d];d++)if(v.active)for(var m=0,g;g=v.lines[m];m++)e.fillText(p[m].text,g[0],g[1]);else h.splice(d--,1);h.length==0&&delete a[l]}}}e.restore()},u.prototype.getTextInfo=function(n,i,s,o,u){if(!t.getOptions().canvas)return r.call(this,n,i,s,o,u);var a,f,l,c;i=""+i,typeof s=="object"?a=s.style+" "+s.variant+" "+s.weight+" "+s.size+"px "+s.family:a=s,f=this._textCache[n],f==null&&(f=this._textCache[n]={}),l=f[a],l==null&&(l=f[a]={}),c=l[i];if(c==null){var h=this.context;if(typeof s!="object"){var p=e("<div>&nbsp;</div>").css("position","absolute").addClass(typeof s=="string"?s:null).appendTo(this.getTextLayer(n));s={lineHeight:p.height(),style:p.css("font-style"),variant:p.css("font-variant"),weight:p.css("font-weight"),family:p.css("font-family"),color:p.css("color")},s.size=p.css("line-height",1).height(),p.remove()}a=s.style+" "+s.variant+" "+s.weight+" "+s.size+"px "+s.family,c=l[i]={width:0,height:0,positions:[],lines:[],font:{definition:a,color:s.color}},h.save(),h.font=a;var d=(i+"").replace(/<br ?\/?>|\r\n|\r/g,"\n").split("\n");for(var v=0;v<d.length;++v){var m=d[v],g=h.measureText(m);c.width=Math.max(g.width,c.width),c.height+=s.lineHeight,c.lines.push({text:m,width:g.width,height:s.lineHeight})}h.restore()}return c},u.prototype.addText=function(e,n,r,s,o,u,a,f,l){if(!t.getOptions().canvas)return i.call(this,e,n,r,s,o,u,a,f,l);var c=this.getTextInfo(e,s,o,u,a),h=c.positions,p=c.lines;r+=c.height/p.length/2,l=="middle"?r=Math.round(r-c.height/2):l=="bottom"?r=Math.round(r-c.height):r=Math.round(r),!(window.opera&&window.opera.version().split(".")[0]<12)||(r-=2);for(var d=0,v;v=h[d];d++)if(v.x==n&&v.y==r){v.active=!0;return}v={active:!0,lines:[],x:n,y:r},h.push(v);for(var d=0,m;m=p[d];d++)f=="center"?v.lines.push([Math.round(n-m.width/2),r]):f=="right"?v.lines.push([Math.round(n-m.width),r]):v.lines.push([Math.round(n),r]),r+=m.height}}var t={canvas:!0},n,r,i,s=Object.prototype.hasOwnProperty;e.plot.plugins.push({init:o,options:t,name:"canvas",version:"1.0"})})(jQuery);
2891\ No newline at end of file
2892
2893=== added file 'dashboard_app/static/dashboard_app/js/jquery.flot.min.js.OTHER'
2894--- dashboard_app/static/dashboard_app/js/jquery.flot.min.js.OTHER 1970-01-01 00:00:00 +0000
2895+++ dashboard_app/static/dashboard_app/js/jquery.flot.min.js.OTHER 2013-09-23 10:56:27 +0000
2896@@ -0,0 +1,29 @@
2897+/* Javascript plotting library for jQuery, version 0.8.1.
2898+
2899+Copyright (c) 2007-2013 IOLA and Ole Laursen.
2900+Licensed under the MIT license.
2901+
2902+*/// first an inline dependency, jquery.colorhelpers.js, we inline it here
2903+// for convenience
2904+/* Plugin for jQuery for working with colors.
2905+ *
2906+ * Version 1.1.
2907+ *
2908+ * Inspiration from jQuery color animation plugin by John Resig.
2909+ *
2910+ * Released under the MIT license by Ole Laursen, October 2009.
2911+ *
2912+ * Examples:
2913+ *
2914+ * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
2915+ * var c = $.color.extract($("#mydiv"), 'background-color');
2916+ * console.log(c.r, c.g, c.b, c.a);
2917+ * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
2918+ *
2919+ * Note that .scale() and .add() return the same modified object
2920+ * instead of making a new one.
2921+ *
2922+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
2923+ * produce a color rather than just crashing.
2924+ */(function(e){e.color={},e.color.make=function(t,n,r,i){var s={};return s.r=t||0,s.g=n||0,s.b=r||0,s.a=i!=null?i:1,s.add=function(e,t){for(var n=0;n<e.length;++n)s[e.charAt(n)]+=t;return s.normalize()},s.scale=function(e,t){for(var n=0;n<e.length;++n)s[e.charAt(n)]*=t;return s.normalize()},s.toString=function(){return s.a>=1?"rgb("+[s.r,s.g,s.b].join(",")+")":"rgba("+[s.r,s.g,s.b,s.a].join(",")+")"},s.normalize=function(){function e(e,t,n){return t<e?e:t>n?n:t}return s.r=e(0,parseInt(s.r),255),s.g=e(0,parseInt(s.g),255),s.b=e(0,parseInt(s.b),255),s.a=e(0,s.a,1),s},s.clone=function(){return e.color.make(s.r,s.b,s.g,s.a)},s.normalize()},e.color.extract=function(t,n){var r;do{r=t.css(n).toLowerCase();if(r!=""&&r!="transparent")break;t=t.parent()}while(!e.nodeName(t.get(0),"body"));return r=="rgba(0, 0, 0, 0)"&&(r="transparent"),e.color.parse(r)},e.color.parse=function(n){var r,i=e.color.make;if(r=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(n))return i(parseInt(r[1],10),parseInt(r[2],10),parseInt(r[3],10));if(r=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(n))return i(parseInt(r[1],10),parseInt(r[2],10),parseInt(r[3],10),parseFloat(r[4]));if(r=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(n))return i(parseFloat(r[1])*2.55,parseFloat(r[2])*2.55,parseFloat(r[3])*2.55);if(r=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(n))return i(parseFloat(r[1])*2.55,parseFloat(r[2])*2.55,parseFloat(r[3])*2.55,parseFloat(r[4]));if(r=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(n))return i(parseInt(r[1],16),parseInt(r[2],16),parseInt(r[3],16));if(r=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(n))return i(parseInt(r[1]+r[1],16),parseInt(r[2]+r[2],16),parseInt(r[3]+r[3],16));var s=e.trim(n).toLowerCase();return s=="transparent"?i(255,255,255,0):(r=t[s]||[0,0,0],i(r[0],r[1],r[2]))};var t={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery),function(e){function n(t,n){var r=n.children("."+t)[0];if(r==null){r=document.createElement("canvas"),r.className=t,e(r).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(n);if(!r.getContext){if(!window.G_vmlCanvasManager)throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.");r=window.G_vmlCanvasManager.initElement(r)}}this.element=r;var i=this.context=r.getContext("2d"),s=window.devicePixelRatio||1,o=i.webkitBackingStorePixelRatio||i.mozBackingStorePixelRatio||i.msBackingStorePixelRatio||i.oBackingStorePixelRatio||i.backingStorePixelRatio||1;this.pixelRatio=s/o,this.resize(n.width(),n.height()),this.textContainer=null,this.text={},this._textCache={}}function r(t,r,s,o){function E(e,t){t=[w].concat(t);for(var n=0;n<e.length;++n)e[n].apply(this,t)}function S(){var t={Canvas:n};for(var r=0;r<o.length;++r){var i=o[r];i.init(w,t),i.options&&e.extend(!0,a,i.options)}}function x(n){e.extend(!0,a,n),n&&n.colors&&(a.colors=n.colors),a.xaxis.color==null&&(a.xaxis.color=e.color.parse(a.grid.color).scale("a",.22).toString()),a.yaxis.color==null&&(a.yaxis.color=e.color.parse(a.grid.color).scale("a",.22).toString()),a.xaxis.tickColor==null&&(a.xaxis.tickColor=a.grid.tickColor||a.xaxis.color),a.yaxis.tickColor==null&&(a.yaxis.tickColor=a.grid.tickColor||a.yaxis.color),a.grid.borderColor==null&&(a.grid.borderColor=a.grid.color),a.grid.tickColor==null&&(a.grid.tickColor=e.color.parse(a.grid.color).scale("a",.22).toString());var r,i,s,o={style:t.css("font-style"),size:Math.round(.8*(+t.css("font-size").replace("px","")||13)),variant:t.css("font-variant"),weight:t.css("font-weight"),family:t.css("font-family")};o.lineHeight=o.size*1.15,s=a.xaxes.length||1;for(r=0;r<s;++r)i=a.xaxes[r],i&&!i.tickColor&&(i.tickColor=i.color),i=e.extend(!0,{},a.xaxis,i),a.xaxes[r]=i,i.font&&(i.font=e.extend({},o,i.font),i.font.color||(i.font.color=i.color));s=a.yaxes.length||1;for(r=0;r<s;++r)i=a.yaxes[r],i&&!i.tickColor&&(i.tickColor=i.color),i=e.extend(!0,{},a.yaxis,i),a.yaxes[r]=i,i.font&&(i.font=e.extend({},o,i.font),i.font.color||(i.font.color=i.color));a.xaxis.noTicks&&a.xaxis.ticks==null&&(a.xaxis.ticks=a.xaxis.noTicks),a.yaxis.noTicks&&a.yaxis.ticks==null&&(a.yaxis.ticks=a.yaxis.noTicks),a.x2axis&&(a.xaxes[1]=e.extend(!0,{},a.xaxis,a.x2axis),a.xaxes[1].position="top"),a.y2axis&&(a.yaxes[1]=e.extend(!0,{},a.yaxis,a.y2axis),a.yaxes[1].position="right"),a.grid.coloredAreas&&(a.grid.markings=a.grid.coloredAreas),a.grid.coloredAreasColor&&(a.grid.markingsColor=a.grid.coloredAreasColor),a.lines&&e.extend(!0,a.series.lines,a.lines),a.points&&e.extend(!0,a.series.points,a.points),a.bars&&e.extend(!0,a.series.bars,a.bars),a.shadowSize!=null&&(a.series.shadowSize=a.shadowSize),a.highlightColor!=null&&(a.series.highlightColor=a.highlightColor);for(r=0;r<a.xaxes.length;++r)O(d,r+1).options=a.xaxes[r];for(r=0;r<a.yaxes.length;++r)O(v,r+1).options=a.yaxes[r];for(var u in b)a.hooks[u]&&a.hooks[u].length&&(b[u]=b[u].concat(a.hooks[u]));E(b.processOptions,[a])}function T(e){u=N(e),M(),_()}function N(t){var n=[];for(var r=0;r<t.length;++r){var i=e.extend(!0,{},a.series);t[r].data!=null?(i.data=t[r].data,delete t[r].data,e.extend(!0,i,t[r]),t[r].data=i.data):i.data=t[r],n.push(i)}return n}function C(e,t){var n=e[t+"axis"];return typeof n=="object"&&(n=n.n),typeof n!="number"&&(n=1),n}function k(){return e.grep(d.concat(v),function(e){return e})}function L(e){var t={},n,r;for(n=0;n<d.length;++n)r=d[n],r&&r.used&&(t["x"+r.n]=r.c2p(e.left));for(n=0;n<v.length;++n)r=v[n],r&&r.used&&(t["y"+r.n]=r.c2p(e.top));return t.x1!==undefined&&(t.x=t.x1),t.y1!==undefined&&(t.y=t.y1),t}function A(e){var t={},n,r,i;for(n=0;n<d.length;++n){r=d[n];if(r&&r.used){i="x"+r.n,e[i]==null&&r.n==1&&(i="x");if(e[i]!=null){t.left=r.p2c(e[i]);break}}}for(n=0;n<v.length;++n){r=v[n];if(r&&r.used){i="y"+r.n,e[i]==null&&r.n==1&&(i="y");if(e[i]!=null){t.top=r.p2c(e[i]);break}}}return t}function O(t,n){return t[n-1]||(t[n-1]={n:n,direction:t==d?"x":"y",options:e.extend(!0,{},t==d?a.xaxis:a.yaxis)}),t[n-1]}function M(){var t=u.length,n=-1,r;for(r=0;r<u.length;++r){var i=u[r].color;i!=null&&(t--,typeof i=="number"&&i>n&&(n=i))}t<=n&&(t=n+1);var s,o=[],f=a.colors,l=f.length,c=0;for(r=0;r<t;r++)s=e.color.parse(f[r%l]||"#666"),r%l==0&&r&&(c>=0?c<.5?c=-c-.2:c=0:c=-c),o[r]=s.scale("rgb",1+c);var h=0,p;for(r=0;r<u.length;++r){p=u[r],p.color==null?(p.color=o[h].toString(),++h):typeof p.color=="number"&&(p.color=o[p.color].toString());if(p.lines.show==null){var m,g=!0;for(m in p)if(p[m]&&p[m].show){g=!1;break}g&&(p.lines.show=!0)}p.lines.zero==null&&(p.lines.zero=!!p.lines.fill),p.xaxis=O(d,C(p,"x")),p.yaxis=O(v,C(p,"y"))}}function _(){function x(e,t,n){t<e.datamin&&t!=-r&&(e.datamin=t),n>e.datamax&&n!=r&&(e.datamax=n)}var t=Number.POSITIVE_INFINITY,n=Number.NEGATIVE_INFINITY,r=Number.MAX_VALUE,i,s,o,a,f,l,c,h,p,d,v,m,g,y,w,S;e.each(k(),function(e,r){r.datamin=t,r.datamax=n,r.used=!1});for(i=0;i<u.length;++i)l=u[i],l.datapoints={points:[]},E(b.processRawData,[l,l.data,l.datapoints]);for(i=0;i<u.length;++i){l=u[i],w=l.data,S=l.datapoints.format;if(!S){S=[],S.push({x:!0,number:!0,required:!0}),S.push({y:!0,number:!0,required:!0});if(l.bars.show||l.lines.show&&l.lines.fill){var T=!!(l.bars.show&&l.bars.zero||l.lines.show&&l.lines.zero);S.push({y:!0,number:!0,required:!1,defaultValue:0,autoscale:T}),l.bars.horizontal&&(delete S[S.length-1].y,S[S.length-1].x=!0)}l.datapoints.format=S}if(l.datapoints.pointsize!=null)continue;l.datapoints.pointsize=S.length,h=l.datapoints.pointsize,c=l.datapoints.points;var N=l.lines.show&&l.lines.steps;l.xaxis.used=l.yaxis.used=!0;for(s=o=0;s<w.length;++s,o+=h){y=w[s];var C=y==null;if(!C)for(a=0;a<h;++a)m=y[a],g=S[a],g&&(g.number&&m!=null&&(m=+m,isNaN(m)?m=null:m==Infinity?m=r:m==-Infinity&&(m=-r)),m==null&&(g.required&&(C=!0),g.defaultValue!=null&&(m=g.defaultValue))),c[o+a]=m;if(C)for(a=0;a<h;++a)m=c[o+a],m!=null&&(g=S[a],g.autoscale&&(g.x&&x(l.xaxis,m,m),g.y&&x(l.yaxis,m,m))),c[o+a]=null;else if(N&&o>0&&c[o-h]!=null&&c[o-h]!=c[o]&&c[o-h+1]!=c[o+1]){for(a=0;a<h;++a)c[o+h+a]=c[o+a];c[o+1]=c[o-h+1],o+=h}}}for(i=0;i<u.length;++i)l=u[i],E(b.processDatapoints,[l,l.datapoints]);for(i=0;i<u.length;++i){l=u[i],c=l.datapoints.points,h=l.datapoints.pointsize,S=l.datapoints.format;var L=t,A=t,O=n,M=n;for(s=0;s<c.length;s+=h){if(c[s]==null)continue;for(a=0;a<h;++a){m=c[s+a],g=S[a];if(!g||g.autoscale===!1||m==r||m==-r)continue;g.x&&(m<L&&(L=m),m>O&&(O=m)),g.y&&(m<A&&(A=m),m>M&&(M=m))}}if(l.bars.show){var _;switch(l.bars.align){case"left":_=0;break;case"right":_=-l.bars.barWidth;break;case"center":_=-l.bars.barWidth/2;break;default:throw new Error("Invalid bar alignment: "+l.bars.align)}l.bars.horizontal?(A+=_,M+=_+l.bars.barWidth):(L+=_,O+=_+l.bars.barWidth)}x(l.xaxis,L,O),x(l.yaxis,A,M)}e.each(k(),function(e,r){r.datamin==t&&(r.datamin=null),r.datamax==n&&(r.datamax=null)})}function D(){t.css("padding",0).children(":not(.flot-base,.flot-overlay)").remove(),t.css("position")=="static"&&t.css("position","relative"),f=new n("flot-base",t),l=new n("flot-overlay",t),h=f.context,p=l.context,c=e(l.element).unbind();var r=t.data("plot");r&&(r.shutdown(),l.clear()),t.data("plot",w)}function P(){a.grid.hoverable&&(c.mousemove(at),c.bind("mouseleave",ft)),a.grid.clickable&&c.click(lt),E(b.bindEvents,[c])}function H(){ot&&clearTimeout(ot),c.unbind("mousemove",at),c.unbind("mouseleave",ft),c.unbind("click",lt),E(b.shutdown,[c])}function B(e){function t(e){return e}var n,r,i=e.options.transform||t,s=e.options.inverseTransform;e.direction=="x"?(n=e.scale=g/Math.abs(i(e.max)-i(e.min)),r=Math.min(i(e.max),i(e.min))):(n=e.scale=y/Math.abs(i(e.max)-i(e.min)),n=-n,r=Math.max(i(e.max),i(e.min))),i==t?e.p2c=function(e){return(e-r)*n}:e.p2c=function(e){return(i(e)-r)*n},s?e.c2p=function(e){return s(r+e/n)}:e.c2p=function(e){return r+e/n}}function j(e){var t=e.options,n=e.ticks||[],r=t.labelWidth||0,i=t.labelHeight||0,s=r||e.direction=="x"?Math.floor(f.width/(n.length||1)):null;legacyStyles=e.direction+"Axis "+e.direction+e.n+"Axis",layer="flot-"+e.direction+"-axis flot-"+e.direction+e.n+"-axis "+legacyStyles,font=t.font||"flot-tick-label tickLabel";for(var o=0;o<n.length;++o){var u=n[o];if(!u.label)continue;var a=f.getTextInfo(layer,u.label,font,null,s);r=Math.max(r,a.width),i=Math.max(i,a.height)}e.labelWidth=t.labelWidth||r,e.labelHeight=t.labelHeight||i}function F(t){var n=t.labelWidth,r=t.labelHeight,i=t.options.position,s=t.options.tickLength,o=a.grid.axisMargin,u=a.grid.labelMargin,l=t.direction=="x"?d:v,c,h,p=e.grep(l,function(e){return e&&e.options.position==i&&e.reserveSpace});e.inArray(t,p)==p.length-1&&(o=0);if(s==null){var g=e.grep(l,function(e){return e&&e.reserveSpace});h=e.inArray(t,g)==0,h?s="full":s=5}isNaN(+s)||(u+=+s),t.direction=="x"?(r+=u,i=="bottom"?(m.bottom+=r+o,t.box={top:f.height-m.bottom,height:r}):(t.box={top:m.top+o,height:r},m.top+=r+o)):(n+=u,i=="left"?(t.box={left:m.left+o,width:n},m.left+=n+o):(m.right+=n+o,t.box={left:f.width-m.right,width:n})),t.position=i,t.tickLength=s,t.box.padding=u,t.innermost=h}function I(e){e.direction=="x"?(e.box.left=m.left-e.labelWidth/2,e.box.width=f.width-m.left-m.right+e.labelWidth):(e.box.top=m.top-e.labelHeight/2,e.box.height=f.height-m.bottom-m.top+e.labelHeight)}function q(){var t=a.grid.minBorderMargin,n={x:0,y:0},r,i;if(t==null){t=0;for(r=0;r<u.length;++r)t=Math.max(t,2*(u[r].points.radius+u[r].points.lineWidth/2))}n.x=n.y=Math.ceil(t),e.each(k(),function(e,t){var r=t.direction;t.reserveSpace&&(n[r]=Math.ceil(Math.max(n[r],(r=="x"?t.labelWidth:t.labelHeight)/2)))}),m.left=Math.max(n.x,m.left),m.right=Math.max(n.x,m.right),m.top=Math.max(n.y,m.top),m.bottom=Math.max(n.y,m.bottom)}function R(){var t,n=k(),r=a.grid.show;for(var i in m){var s=a.grid.margin||0;m[i]=typeof s=="number"?s:s[i]||0}E(b.processOffset,[m]);for(var i in m)typeof a.grid.borderWidth=="object"?m[i]+=r?a.grid.borderWidth[i]:0:m[i]+=r?a.grid.borderWidth:0;e.each(n,function(e,t){t.show=t.options.show,t.show==null&&(t.show=t.used),t.reserveSpace=t.show||t.options.reserveSpace,U(t)});if(r){var o=e.grep(n,function(e){return e.reserveSpace});e.each(o,function(e,t){z(t),W(t),X(t,t.ticks),j(t)});for(t=o.length-1;t>=0;--t)F(o[t]);q(),e.each(o,function(e,t){I(t)})}g=f.width-m.left-m.right,y=f.height-m.bottom-m.top,e.each(n,function(e,t){B(t)}),r&&G(),it()}function U(e){var t=e.options,n=+(t.min!=null?t.min:e.datamin),r=+(t.max!=null?t.max:e.datamax),i=r-n;if(i==0){var s=r==0?1:.01;t.min==null&&(n-=s);if(t.max==null||t.min!=null)r+=s}else{var o=t.autoscaleMargin;o!=null&&(t.min==null&&(n-=i*o,n<0&&e.datamin!=null&&e.datamin>=0&&(n=0)),t.max==null&&(r+=i*o,r>0&&e.datamax!=null&&e.datamax<=0&&(r=0)))}e.min=n,e.max=r}function z(t){var n=t.options,r;typeof n.ticks=="number"&&n.ticks>0?r=n.ticks:r=.3*Math.sqrt(t.direction=="x"?f.width:f.height);var s=(t.max-t.min)/r,o=-Math.floor(Math.log(s)/Math.LN10),u=n.tickDecimals;u!=null&&o>u&&(o=u);var a=Math.pow(10,-o),l=s/a,c;l<1.5?c=1:l<3?(c=2,l>2.25&&(u==null||o+1<=u)&&(c=2.5,++o)):l<7.5?c=5:c=10,c*=a,n.minTickSize!=null&&c<n.minTickSize&&(c=n.minTickSize),t.delta=s,t.tickDecimals=Math.max(0,u!=null?u:o),t.tickSize=n.tickSize||c;if(n.mode=="time"&&!t.tickGenerator)throw new Error("Time mode requires the flot.time plugin.");t.tickGenerator||(t.tickGenerator=function(e){var t=[],n=i(e.min,e.tickSize),r=0,s=Number.NaN,o;do o=s,s=n+r*e.tickSize,t.push(s),++r;while(s<e.max&&s!=o);return t},t.tickFormatter=function(e,t){var n=t.tickDecimals?Math.pow(10,t.tickDecimals):1,r=""+Math.round(e*n)/n;if(t.tickDecimals!=null){var i=r.indexOf("."),s=i==-1?0:r.length-i-1;if(s<t.tickDecimals)return(s?r:r+".")+(""+n).substr(1,t.tickDecimals-s)}return r}),e.isFunction(n.tickFormatter)&&(t.tickFormatter=function(e,t){return""+n.tickFormatter(e,t)});if(n.alignTicksWithAxis!=null){var h=(t.direction=="x"?d:v)[n.alignTicksWithAxis-1];if(h&&h.used&&h!=t){var p=t.tickGenerator(t);p.length>0&&(n.min==null&&(t.min=Math.min(t.min,p[0])),n.max==null&&p.length>1&&(t.max=Math.max(t.max,p[p.length-1]))),t.tickGenerator=function(e){var t=[],n,r;for(r=0;r<h.ticks.length;++r)n=(h.ticks[r].v-h.min)/(h.max-h.min),n=e.min+n*(e.max-e.min),t.push(n);return t};if(!t.mode&&n.tickDecimals==null){var m=Math.max(0,-Math.floor(Math.log(t.delta)/Math.LN10)+1),g=t.tickGenerator(t);g.length>1&&/\..*0$/.test((g[1]-g[0]).toFixed(m))||(t.tickDecimals=m)}}}}function W(t){var n=t.options.ticks,r=[];n==null||typeof n=="number"&&n>0?r=t.tickGenerator(t):n&&(e.isFunction(n)?r=n(t):r=n);var i,s;t.ticks=[];for(i=0;i<r.length;++i){var o=null,u=r[i];typeof u=="object"?(s=+u[0],u.length>1&&(o=u[1])):s=+u,o==null&&(o=t.tickFormatter(s,t)),isNaN(s)||t.ticks.push({v:s,label:o})}}function X(e,t){e.options.autoscaleMargin&&t.length>0&&(e.options.min==null&&(e.min=Math.min(e.min,t[0].v)),e.options.max==null&&t.length>1&&(e.max=Math.max(e.max,t[t.length-1].v)))}function V(){f.clear(),E(b.drawBackground,[h]);var e=a.grid;e.show&&e.backgroundColor&&K(),e.show&&!e.aboveData&&Q();for(var t=0;t<u.length;++t)E(b.drawSeries,[h,u[t]]),Y(u[t]);E(b.draw,[h]),e.show&&e.aboveData&&Q(),f.render(),ht()}function J(e,t){var n,r,i,s,o=k();for(var u=0;u<o.length;++u){n=o[u];if(n.direction==t){s=t+n.n+"axis",!e[s]&&n.n==1&&(s=t+"axis");if(e[s]){r=e[s].from,i=e[s].to;break}}}e[s]||(n=t=="x"?d[0]:v[0],r=e[t+"1"],i=e[t+"2"]);if(r!=null&&i!=null&&r>i){var a=r;r=i,i=a}return{from:r,to:i,axis:n}}function K(){h.save(),h.translate(m.left,m.top),h.fillStyle=bt(a.grid.backgroundColor,y,0,"rgba(255, 255, 255, 0)"),h.fillRect(0,0,g,y),h.restore()}function Q(){var t,n,r,i;h.save(),h.translate(m.left,m.top);var s=a.grid.markings;if(s){e.isFunction(s)&&(n=w.getAxes(),n.xmin=n.xaxis.min,n.xmax=n.xaxis.max,n.ymin=n.yaxis.min,n.ymax=n.yaxis.max,s=s(n));for(t=0;t<s.length;++t){var o=s[t],u=J(o,"x"),f=J(o,"y");u.from==null&&(u.from=u.axis.min),u.to==null&&(u.to=u.axis.max),f.from==null&&(f.from=f.axis.min),f.to==null&&(f.to=f.axis.max);if(u.to<u.axis.min||u.from>u.axis.max||f.to<f.axis.min||f.from>f.axis.max)continue;u.from=Math.max(u.from,u.axis.min),u.to=Math.min(u.to,u.axis.max),f.from=Math.max(f.from,f.axis.min),f.to=Math.min(f.to,f.axis.max);if(u.from==u.to&&f.from==f.to)continue;u.from=u.axis.p2c(u.from),u.to=u.axis.p2c(u.to),f.from=f.axis.p2c(f.from),f.to=f.axis.p2c(f.to),u.from==u.to||f.from==f.to?(h.beginPath(),h.strokeStyle=o.color||a.grid.markingsColor,h.lineWidth=o.lineWidth||a.grid.markingsLineWidth,h.moveTo(u.from,f.from),h.lineTo(u.to,f.to),h.stroke()):(h.fillStyle=o.color||a.grid.markingsColor,h.fillRect(u.from,f.to,u.to-u.from,f.from-f.to))}}n=k(),r=a.grid.borderWidth;for(var l=0;l<n.length;++l){var c=n[l],p=c.box,d=c.tickLength,v,b,E,S;if(!c.show||c.ticks.length==0)continue;h.lineWidth=1,c.direction=="x"?(v=0,d=="full"?b=c.position=="top"?0:y:b=p.top-m.top+(c.position=="top"?p.height:0)):(b=0,d=="full"?v=c.position=="left"?0:g:v=p.left-m.left+(c.position=="left"?p.width:0)),c.innermost||(h.strokeStyle=c.options.color,h.beginPath(),E=S=0,c.direction=="x"?E=g+1:S=y+1,h.lineWidth==1&&(c.direction=="x"?b=Math.floor(b)+.5:v=Math.floor(v)+.5),h.moveTo(v,b),h.lineTo(v+E,b+S),h.stroke()),h.strokeStyle=c.options.tickColor,h.beginPath();for(t=0;t<c.ticks.length;++t){var x=c.ticks[t].v;E=S=0;if(isNaN(x)||x<c.min||x>c.max||d=="full"&&(typeof r=="object"&&r[c.position]>0||r>0)&&(x==c.min||x==c.max))continue;c.direction=="x"?(v=c.p2c(x),S=d=="full"?-y:d,c.position=="top"&&(S=-S)):(b=c.p2c(x),E=d=="full"?-g:d,c.position=="left"&&(E=-E)),h.lineWidth==1&&(c.direction=="x"?v=Math.floor(v)+.5:b=Math.floor(b)+.5),h.moveTo(v,b),h.lineTo(v+E,b+S)}h.stroke()}r&&(i=a.grid.borderColor,typeof r=="object"||typeof i=="object"?(typeof r!="object"&&(r={top:r,right:r,bottom:r,left:r}),typeof i!="object"&&(i={top:i,right:i,bottom:i,left:i}),r.top>0&&(h.strokeStyle=i.top,h.lineWidth=r.top,h.beginPath(),h.moveTo(0-r.left,0-r.top/2),h.lineTo(g,0-r.top/2),h.stroke()),r.right>0&&(h.strokeStyle=i.right,h.lineWidth=r.right,h.beginPath(),h.moveTo(g+r.right/2,0-r.top),h.lineTo(g+r.right/2,y),h.stroke()),r.bottom>0&&(h.strokeStyle=i.bottom,h.lineWidth=r.bottom,h.beginPath(),h.moveTo(g+r.right,y+r.bottom/2),h.lineTo(0,y+r.bottom/2),h.stroke()),r.left>0&&(h.strokeStyle=i.left,h.lineWidth=r.left,h.beginPath(),h.moveTo(0-r.left/2,y+r.bottom),h.lineTo(0-r.left/2,0),h.stroke())):(h.lineWidth=r,h.strokeStyle=a.grid.borderColor,h.strokeRect(-r/2,-r/2,g+r,y+r))),h.restore()}function G(){e.each(k(),function(e,t){if(!t.show||t.ticks.length==0)return;var n=t.box,r=t.direction+"Axis "+t.direction+t.n+"Axis",i="flot-"+t.direction+"-axis flot-"+t.direction+t.n+"-axis "+r,s=t.options.font||"flot-tick-label tickLabel",o,u,a,l,c;f.removeText(i);for(var h=0;h<t.ticks.length;++h){o=t.ticks[h];if(!o.label||o.v<t.min||o.v>t.max)continue;t.direction=="x"?(l="center",u=m.left+t.p2c(o.v),t.position=="bottom"?a=n.top+n.padding:(a=n.top+n.height-n.padding,c="bottom")):(c="middle",a=m.top+t.p2c(o.v),t.position=="left"?(u=n.left+n.width-n.padding,l="right"):u=n.left+n.padding),f.addText(i,u,a,o.label,s,null,null,l,c)}})}function Y(e){e.lines.show&&Z(e),e.bars.show&&nt(e),e.points.show&&et(e)}function Z(e){function t(e,t,n,r,i){var s=e.points,o=e.pointsize,u=null,a=null;h.beginPath();for(var f=o;f<s.length;f+=o){var l=s[f-o],c=s[f-o+1],p=s[f],d=s[f+1];if(l==null||p==null)continue;if(c<=d&&c<i.min){if(d<i.min)continue;l=(i.min-c)/(d-c)*(p-l)+l,c=i.min}else if(d<=c&&d<i.min){if(c<i.min)continue;p=(i.min-c)/(d-c)*(p-l)+l,d=i.min}if(c>=d&&c>i.max){if(d>i.max)continue;l=(i.max-c)/(d-c)*(p-l)+l,c=i.max}else if(d>=c&&d>i.max){if(c>i.max)continue;p=(i.max-c)/(d-c)*(p-l)+l,d=i.max}if(l<=p&&l<r.min){if(p<r.min)continue;c=(r.min-l)/(p-l)*(d-c)+c,l=r.min}else if(p<=l&&p<r.min){if(l<r.min)continue;d=(r.min-l)/(p-l)*(d-c)+c,p=r.min}if(l>=p&&l>r.max){if(p>r.max)continue;c=(r.max-l)/(p-l)*(d-c)+c,l=r.max}else if(p>=l&&p>r.max){if(l>r.max)continue;d=(r.max-l)/(p-l)*(d-c)+c,p=r.max}(l!=u||c!=a)&&h.moveTo(r.p2c(l)+t,i.p2c(c)+n),u=p,a=d,h.lineTo(r.p2c(p)+t,i.p2c(d)+n)}h.stroke()}function n(e,t,n){var r=e.points,i=e.pointsize,s=Math.min(Math.max(0,n.min),n.max),o=0,u,a=!1,f=1,l=0,c=0;for(;;){if(i>0&&o>r.length+i)break;o+=i;var p=r[o-i],d=r[o-i+f],v=r[o],m=r[o+f];if(a){if(i>0&&p!=null&&v==null){c=o,i=-i,f=2;continue}if(i<0&&o==l+i){h.fill(),a=!1,i=-i,f=1,o=l=c+i;continue}}if(p==null||v==null)continue;if(p<=v&&p<t.min){if(v<t.min)continue;d=(t.min-p)/(v-p)*(m-d)+d,p=t.min}else if(v<=p&&v<t.min){if(p<t.min)continue;m=(t.min-p)/(v-p)*(m-d)+d,v=t.min}if(p>=v&&p>t.max){if(v>t.max)continue;d=(t.max-p)/(v-p)*(m-d)+d,p=t.max}else if(v>=p&&v>t.max){if(p>t.max)continue;m=(t.max-p)/(v-p)*(m-d)+d,v=t.max}a||(h.beginPath(),h.moveTo(t.p2c(p),n.p2c(s)),a=!0);if(d>=n.max&&m>=n.max){h.lineTo(t.p2c(p),n.p2c(n.max)),h.lineTo(t.p2c(v),n.p2c(n.max));continue}if(d<=n.min&&m<=n.min){h.lineTo(t.p2c(p),n.p2c(n.min)),h.lineTo(t.p2c(v),n.p2c(n.min));continue}var g=p,y=v;d<=m&&d<n.min&&m>=n.min?(p=(n.min-d)/(m-d)*(v-p)+p,d=n.min):m<=d&&m<n.min&&d>=n.min&&(v=(n.min-d)/(m-d)*(v-p)+p,m=n.min),d>=m&&d>n.max&&m<=n.max?(p=(n.max-d)/(m-d)*(v-p)+p,d=n.max):m>=d&&m>n.max&&d<=n.max&&(v=(n.max-d)/(m-d)*(v-p)+p,m=n.max),p!=g&&h.lineTo(t.p2c(g),n.p2c(d)),h.lineTo(t.p2c(p),n.p2c(d)),h.lineTo(t.p2c(v),n.p2c(m)),v!=y&&(h.lineTo(t.p2c(v),n.p2c(m)),h.lineTo(t.p2c(y),n.p2c(m)))}}h.save(),h.translate(m.left,m.top),h.lineJoin="round";var r=e.lines.lineWidth,i=e.shadowSize;if(r>0&&i>0){h.lineWidth=i,h.strokeStyle="rgba(0,0,0,0.1)";var s=Math.PI/18;t(e.datapoints,Math.sin(s)*(r/2+i/2),Math.cos(s)*(r/2+i/2),e.xaxis,e.yaxis),h.lineWidth=i/2,t(e.datapoints,Math.sin(s)*(r/2+i/4),Math.cos(s)*(r/2+i/4),e.xaxis,e.yaxis)}h.lineWidth=r,h.strokeStyle=e.color;var o=rt(e.lines,e.color,0,y);o&&(h.fillStyle=o,n(e.datapoints,e.xaxis,e.yaxis)),r>0&&t(e.datapoints,0,0,e.xaxis,e.yaxis),h.restore()}function et(e){function t(e,t,n,r,i,s,o,u){var a=e.points,f=e.pointsize;for(var l=0;l<a.length;l+=f){var c=a[l],p=a[l+1];if(c==null||c<s.min||c>s.max||p<o.min||p>o.max)continue;h.beginPath(),c=s.p2c(c),p=o.p2c(p)+r,u=="circle"?h.arc(c,p,t,0,i?Math.PI:Math.PI*2,!1):u(h,c,p,t,i),h.closePath(),n&&(h.fillStyle=n,h.fill()),h.stroke()}}h.save(),h.translate(m.left,m.top);var n=e.points.lineWidth,r=e.shadowSize,i=e.points.radius,s=e.points.symbol;n==0&&(n=1e-4);if(n>0&&r>0){var o=r/2;h.lineWidth=o,h.strokeStyle="rgba(0,0,0,0.1)",t(e.datapoints,i,null,o+o/2,!0,e.xaxis,e.yaxis,s),h.strokeStyle="rgba(0,0,0,0.2)",t(e.datapoints,i,null,o/2,!0,e.xaxis,e.yaxis,s)}h.lineWidth=n,h.strokeStyle=e.color,t(e.datapoints,i,rt(e.points,e.color),0,!1,e.xaxis,e.yaxis,s),h.restore()}function tt(e,t,n,r,i,s,o,u,a,f,l,c){var h,p,d,v,m,g,y,b,w;l?(b=g=y=!0,m=!1,h=n,p=e,v=t+r,d=t+i,p<h&&(w=p,p=h,h=w,m=!0,g=!1)):(m=g=y=!0,b=!1,h=e+r,p=e+i,d=n,v=t,v<d&&(w=v,v=d,d=w,b=!0,y=!1));if(p<u.min||h>u.max||v<a.min||d>a.max)return;h<u.min&&(h=u.min,m=!1),p>u.max&&(p=u.max,g=!1),d<a.min&&(d=a.min,b=!1),v>a.max&&(v=a.max,y=!1),h=u.p2c(h),d=a.p2c(d),p=u.p2c(p),v=a.p2c(v),o&&(f.beginPath(),f.moveTo(h,d),f.lineTo(h,v),f.lineTo(p,v),f.lineTo(p,d),f.fillStyle=o(d,v),f.fill()),c>0&&(m||g||y||b)&&(f.beginPath(),f.moveTo(h,d+s),m?f.lineTo(h,v+s):f.moveTo(h,v+s),y?f.lineTo(p,v+s):f.moveTo(p,v+s),g?f.lineTo(p,d+s):f.moveTo(p,d+s),b?f.lineTo(h,d+s):f.moveTo(h,d+s),f.stroke())}function nt(e){function t(t,n,r,i,s,o,u){var a=t.points,f=t.pointsize;for(var l=0;l<a.length;l+=f){if(a[l]==null)continue;tt(a[l],a[l+1],a[l+2],n,r,i,s,o,u,h,e.bars.horizontal,e.bars.lineWidth)}}h.save(),h.translate(m.left,m.top),h.lineWidth=e.bars.lineWidth,h.strokeStyle=e.color;var n;switch(e.bars.align){case"left":n=0;break;case"right":n=-e.bars.barWidth;break;case"center":n=-e.bars.barWidth/2;break;default:throw new Error("Invalid bar alignment: "+e.bars.align)}var r=e.bars.fill?function(t,n){return rt(e.bars,e.color,t,n)}:null;t(e.datapoints,n,n+e.bars.barWidth,0,r,e.xaxis,e.yaxis),h.restore()}function rt(t,n,r,i){var s=t.fill;if(!s)return null;if(t.fillColor)return bt(t.fillColor,r,i,n);var o=e.color.parse(n);return o.a=typeof s=="number"?s:.4,o.normalize(),o.toString()}function it(){t.find(".legend").remove();if(!a.legend.show)return;var n=[],r=[],i=!1,s=a.legend.labelFormatter,o,f;for(var l=0;l<u.length;++l)o=u[l],o.label&&(f=s?s(o.label,o):o.label,f&&r.push({label:f,color:o.color}));if(a.legend.sorted)if(e.isFunction(a.legend.sorted))r.sort(a.legend.sorted);else if(a.legend.sorted=="reverse")r.reverse();else{var c=a.legend.sorted!="descending";r.sort(function(e,t){return e.label==t.label?0:e.label<t.label!=c?1:-1})}for(var l=0;l<r.length;++l){var h=r[l];l%a.legend.noColumns==0&&(i&&n.push("</tr>"),n.push("<tr>"),i=!0),n.push('<td class="legendColorBox"><div style="border:1px solid '+a.legend.labelBoxBorderColor+';padding:1px"><div style="width:4px;height:0;border:5px solid '+h.color+';overflow:hidden"></div></div></td>'+'<td class="legendLabel">'+h.label+"</td>")}i&&n.push("</tr>");if(n.length==0)return;var p='<table style="font-size:smaller;color:'+a.grid.color+'">'+n.join("")+"</table>";if(a.legend.container!=null)e(a.legend.container).html(p);else{var d="",v=a.legend.position,g=a.legend.margin;g[0]==null&&(g=[g,g]),v.charAt(0)=="n"?d+="top:"+(g[1]+m.top)+"px;":v.charAt(0)=="s"&&(d+="bottom:"+(g[1]+m.bottom)+"px;"),v.charAt(1)=="e"?d+="right:"+(g[0]+m.right)+"px;":v.charAt(1)=="w"&&(d+="left:"+(g[0]+m.left)+"px;");var y=e('<div class="legend">'+p.replace('style="','style="position:absolute;'+d+";")+"</div>").appendTo(t);if(a.legend.backgroundOpacity!=0){var b=a.legend.backgroundColor;b==null&&(b=a.grid.backgroundColor,b&&typeof b=="string"?b=e.color.parse(b):b=e.color.extract(y,"background-color"),b.a=1,b=b.toString());var w=y.children();e('<div style="position:absolute;width:'+w.width()+"px;height:"+w.height()+"px;"+d+"background-color:"+b+';"> </div>').prependTo(y).css("opacity",a.legend.backgroundOpacity)}}}function ut(e,t,n){var r=a.grid.mouseActiveRadius,i=r*r+1,s=null,o=!1,f,l,c;for(f=u.length-1;f>=0;--f){if(!n(u[f]))continue;var h=u[f],p=h.xaxis,d=h.yaxis,v=h.datapoints.points,m=p.c2p(e),g=d.c2p(t),y=r/p.scale,b=r/d.scale;c=h.datapoints.pointsize,p.options.inverseTransform&&(y=Number.MAX_VALUE),d.options.inverseTransform&&(b=Number.MAX_VALUE);if(h.lines.show||h.points.show)for(l=0;l<v.length;l+=c){var w=v[l],E=v[l+1];if(w==null)continue;if(w-m>y||w-m<-y||E-g>b||E-g<-b)continue;var S=Math.abs(p.p2c(w)-e),x=Math.abs(d.p2c(E)-t),T=S*S+x*x;T<i&&(i=T,s=[f,l/c])}if(h.bars.show&&!s){var N=h.bars.align=="left"?0:-h.bars.barWidth/2,C=N+h.bars.barWidth;for(l=0;l<v.length;l+=c){var w=v[l],E=v[l+1],k=v[l+2];if(w==null)continue;if(u[f].bars.horizontal?m<=Math.max(k,w)&&m>=Math.min(k,w)&&g>=E+N&&g<=E+C:m>=w+N&&m<=w+C&&g>=Math.min(k,E)&&g<=Math.max(k,E))s=[f,l/c]}}}return s?(f=s[0],l=s[1],c=u[f].datapoints.pointsize,{datapoint:u[f].datapoints.points.slice(l*c,(l+1)*c),dataIndex:l,series:u[f],seriesIndex:f}):null}function at(e){a.grid.hoverable&&ct("plothover",e,function(e){return e["hoverable"]!=0})}function ft(e){a.grid.hoverable&&ct("plothover",e,function(e){return!1})}function lt(e){ct("plotclick",e,function(e){return e["clickable"]!=0})}function ct(e,n,r){var i=c.offset(),s=n.pageX-i.left-m.left,o=n.pageY-i.top-m.top,u=L({left:s,top:o});u.pageX=n.pageX,u.pageY=n.pageY;var f=ut(s,o,r);f&&(f.pageX=parseInt(f.series.xaxis.p2c(f.datapoint[0])+i.left+m.left,10),f.pageY=parseInt(f.series.yaxis.p2c(f.datapoint[1])+i.top+m.top,10));if(a.grid.autoHighlight){for(var l=0;l<st.length;++l){var h=st[l];h.auto==e&&(!f||h.series!=f.series||h.point[0]!=f.datapoint[0]||h.point[1]!=f.datapoint[1])&&vt(h.series,h.point)}f&&dt(f.series,f.datapoint,e)}t.trigger(e,[u,f])}function ht(){var e=a.interaction.redrawOverlayInterval;if(e==-1){pt();return}ot||(ot=setTimeout(pt,e))}function pt(){ot=null,p.save(),l.clear(),p.translate(m.left,m.top);var e,t;for(e=0;e<st.length;++e)t=st[e],t.series.bars.show?yt(t.series,t.point):gt(t.series,t.point);p.restore(),E(b.drawOverlay,[p])}function dt(e,t,n){typeof e=="number"&&(e=u[e]);if(typeof t=="number"){var r=e.datapoints.pointsize;t=e.datapoints.points.slice(r*t,r*(t+1))}var i=mt(e,t);i==-1?(st.push({series:e,point:t,auto:n}),ht()):n||(st[i].auto=!1)}function vt(e,t){if(e==null&&t==null){st=[],ht();return}typeof e=="number"&&(e=u[e]);if(typeof t=="number"){var n=e.datapoints.pointsize;t=e.datapoints.points.slice(n*t,n*(t+1))}var r=mt(e,t);r!=-1&&(st.splice(r,1),ht())}function mt(e,t){for(var n=0;n<st.length;++n){var r=st[n];if(r.series==e&&r.point[0]==t[0]&&r.point[1]==t[1])return n}return-1}function gt(t,n){var r=n[0],i=n[1],s=t.xaxis,o=t.yaxis,u=typeof t.highlightColor=="string"?t.highlightColor:e.color.parse(t.color).scale("a",.5).toString();if(r<s.min||r>s.max||i<o.min||i>o.max)return;var a=t.points.radius+t.points.lineWidth/2;p.lineWidth=a,p.strokeStyle=u;var f=1.5*a;r=s.p2c(r),i=o.p2c(i),p.beginPath(),t.points.symbol=="circle"?p.arc(r,i,f,0,2*Math.PI,!1):t.points.symbol(p,r,i,f,!1),p.closePath(),p.stroke()}function yt(t,n){var r=typeof t.highlightColor=="string"?t.highlightColor:e.color.parse(t.color).scale("a",.5).toString(),i=r,s=t.bars.align=="left"?0:-t.bars.barWidth/2;p.lineWidth=t.bars.lineWidth,p.strokeStyle=r,tt(n[0],n[1],n[2]||0,s,s+t.bars.barWidth,0,function(){return i},t.xaxis,t.yaxis,p,t.bars.horizontal,t.bars.lineWidth)}function bt(t,n,r,i){if(typeof t=="string")return t;var s=h.createLinearGradient(0,r,0,n);for(var o=0,u=t.colors.length;o<u;++o){var a=t.colors[o];if(typeof a!="string"){var f=e.color.parse(i);a.brightness!=null&&(f=f.scale("rgb",a.brightness)),a.opacity!=null&&(f.a*=a.opacity),a=f.toString()}s.addColorStop(o/(u-1),a)}return s}var u=[],a={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:!0,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:.85,sorted:null},xaxis:{show:null,position:"bottom",mode:null,font:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null},yaxis:{autoscaleMargin:.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:!1,radius:3,lineWidth:2,fill:!0,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:!1,fillColor:null,steps:!1},bars:{show:!1,lineWidth:2,barWidth:1,fill:!0,fillColor:null,align:"left",horizontal:!1,zero:!0},shadowSize:3,highlightColor:null},grid:{show:!0,aboveData:!1,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,margin:0,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:!1,hoverable:!1,autoHighlight:!0,mouseActiveRadius:10},interaction:{redrawOverlayInterval:1e3/60},hooks:{}},f=null,l=null,c=null,h=null,p=null,d=[],v=[],m={left:0,right:0,top:0,bottom
2925+:0},g=0,y=0,b={processOptions:[],processRawData:[],processDatapoints:[],processOffset:[],drawBackground:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},w=this;w.setData=T,w.setupGrid=R,w.draw=V,w.getPlaceholder=function(){return t},w.getCanvas=function(){return f.element},w.getPlotOffset=function(){return m},w.width=function(){return g},w.height=function(){return y},w.offset=function(){var e=c.offset();return e.left+=m.left,e.top+=m.top,e},w.getData=function(){return u},w.getAxes=function(){var t={},n;return e.each(d.concat(v),function(e,n){n&&(t[n.direction+(n.n!=1?n.n:"")+"axis"]=n)}),t},w.getXAxes=function(){return d},w.getYAxes=function(){return v},w.c2p=L,w.p2c=A,w.getOptions=function(){return a},w.highlight=dt,w.unhighlight=vt,w.triggerRedrawOverlay=ht,w.pointOffset=function(e){return{left:parseInt(d[C(e,"x")-1].p2c(+e.x)+m.left,10),top:parseInt(v[C(e,"y")-1].p2c(+e.y)+m.top,10)}},w.shutdown=H,w.resize=function(){var e=t.width(),n=t.height();f.resize(e,n),l.resize(e,n)},w.hooks=b,S(w),x(s),D(),T(r),R(),V(),P();var st=[],ot=null}function i(e,t){return t*Math.floor(e/t)}var t=Object.prototype.hasOwnProperty;n.prototype.resize=function(e,t){if(e<=0||t<=0)throw new Error("Invalid dimensions for plot, width = "+e+", height = "+t);var n=this.element,r=this.context,i=this.pixelRatio;this.width!=e&&(n.width=e*i,n.style.width=e+"px",this.width=e),this.height!=t&&(n.height=t*i,n.style.height=t+"px",this.height=t),r.restore(),r.save(),r.scale(i,i)},n.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)},n.prototype.render=function(){var e=this._textCache;for(var n in e)if(t.call(e,n)){var r=this.getTextLayer(n),i=e[n];r.hide();for(var s in i)if(t.call(i,s)){var o=i[s];for(var u in o)if(t.call(o,u)){var a=o[u].positions;for(var f=0,l;l=a[f];f++)l.active?l.rendered||(r.append(l.element),l.rendered=!0):(a.splice(f--,1),l.rendered&&l.element.detach());a.length==0&&delete o[u]}}r.show()}},n.prototype.getTextLayer=function(t){var n=this.text[t];return n==null&&(this.textContainer==null&&(this.textContainer=e("<div class='flot-text'></div>").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)),n=this.text[t]=e("<div></div>").addClass(t).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)),n},n.prototype.getTextInfo=function(t,n,r,i,s){var o,u,a,f;n=""+n,typeof r=="object"?o=r.style+" "+r.variant+" "+r.weight+" "+r.size+"px/"+r.lineHeight+"px "+r.family:o=r,u=this._textCache[t],u==null&&(u=this._textCache[t]={}),a=u[o],a==null&&(a=u[o]={}),f=a[n];if(f==null){var l=e("<div></div>").html(n).css({position:"absolute","max-width":s,top:-9999}).appendTo(this.getTextLayer(t));typeof r=="object"?l.css({font:o,color:r.color}):typeof r=="string"&&l.addClass(r),f=a[n]={width:l.outerWidth(!0),height:l.outerHeight(!0),element:l,positions:[]},l.detach()}return f},n.prototype.addText=function(e,t,n,r,i,s,o,u,a){var f=this.getTextInfo(e,r,i,s,o),l=f.positions;u=="center"?t-=f.width/2:u=="right"&&(t-=f.width),a=="middle"?n-=f.height/2:a=="bottom"&&(n-=f.height);for(var c=0,h;h=l[c];c++)if(h.x==t&&h.y==n){h.active=!0;return}h={active:!0,rendered:!1,element:l.length?f.element.clone():f.element,x:t,y:n},l.push(h),h.element.css({top:Math.round(n),left:Math.round(t),"text-align":u})},n.prototype.removeText=function(e,n,r,i,s,o){if(i==null){var u=this._textCache[e];if(u!=null)for(var a in u)if(t.call(u,a)){var f=u[a];for(var l in f)if(t.call(f,l)){var c=f[l].positions;for(var h=0,p;p=c[h];h++)p.active=!1}}}else{var c=this.getTextInfo(e,i,s,o).positions;for(var h=0,p;p=c[h];h++)p.x==n&&p.y==r&&(p.active=!1)}},e.plot=function(t,n,i){var s=new r(e(t),n,i,e.plot.plugins);return s},e.plot.version="0.8.1",e.plot.plugins=[],e.fn.plot=function(t,n){return this.each(function(){e.plot(this,t,n)})}}(jQuery);
2926\ No newline at end of file
2927
2928=== added file 'dashboard_app/static/dashboard_app/js/jquery.flot.navigate.min.js.OTHER'
2929--- dashboard_app/static/dashboard_app/js/jquery.flot.navigate.min.js.OTHER 1970-01-01 00:00:00 +0000
2930+++ dashboard_app/static/dashboard_app/js/jquery.flot.navigate.min.js.OTHER 2013-09-23 10:56:27 +0000
2931@@ -0,0 +1,86 @@
2932+/* Flot plugin for adding the ability to pan and zoom the plot.
2933+
2934+Copyright (c) 2007-2013 IOLA and Ole Laursen.
2935+Licensed under the MIT license.
2936+
2937+The default behaviour is double click and scrollwheel up/down to zoom in, drag
2938+to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and
2939+plot.pan( offset ) so you easily can add custom controls. It also fires
2940+"plotpan" and "plotzoom" events, useful for synchronizing plots.
2941+
2942+The plugin supports these options:
2943+
2944+ zoom: {
2945+ interactive: false
2946+ trigger: "dblclick" // or "click" for single click
2947+ amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
2948+ }
2949+
2950+ pan: {
2951+ interactive: false
2952+ cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer"
2953+ frameRate: 20
2954+ }
2955+
2956+ xaxis, yaxis, x2axis, y2axis: {
2957+ zoomRange: null // or [ number, number ] (min range, max range) or false
2958+ panRange: null // or [ number, number ] (min, max) or false
2959+ }
2960+
2961+"interactive" enables the built-in drag/click behaviour. If you enable
2962+interactive for pan, then you'll have a basic plot that supports moving
2963+around; the same for zoom.
2964+
2965+"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to
2966+the current viewport.
2967+
2968+"cursor" is a standard CSS mouse cursor string used for visual feedback to the
2969+user when dragging.
2970+
2971+"frameRate" specifies the maximum number of times per second the plot will
2972+update itself while the user is panning around on it (set to null to disable
2973+intermediate pans, the plot will then not update until the mouse button is
2974+released).
2975+
2976+"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange:
2977+[1, 100] the zoom will never scale the axis so that the difference between min
2978+and max is smaller than 1 or larger than 100. You can set either end to null
2979+to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis
2980+will be disabled.
2981+
2982+"panRange" confines the panning to stay within a range, e.g. with panRange:
2983+[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can
2984+be null, e.g. [-10, null]. If you set panRange to false, panning on that axis
2985+will be disabled.
2986+
2987+Example API usage:
2988+
2989+ plot = $.plot(...);
2990+
2991+ // zoom default amount in on the pixel ( 10, 20 )
2992+ plot.zoom({ center: { left: 10, top: 20 } });
2993+
2994+ // zoom out again
2995+ plot.zoomOut({ center: { left: 10, top: 20 } });
2996+
2997+ // zoom 200% in on the pixel (10, 20)
2998+ plot.zoom({ amount: 2, center: { left: 10, top: 20 } });
2999+
3000+ // pan 100 pixels to the left and 20 down
3001+ plot.pan({ left: -100, top: 20 })
3002+
3003+Here, "center" specifies where the center of the zooming should happen. Note
3004+that this is defined in pixel space, not the space of the data points (you can
3005+use the p2c helpers on the axes in Flot to help you convert between these).
3006+
3007+"amount" is the amount to zoom the viewport relative to the current range, so
3008+1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You
3009+can set the default in the options.
3010+
3011+*/// First two dependencies, jquery.event.drag.js and
3012+// jquery.mousewheel.js, we put them inline here to save people the
3013+// effort of downloading them.
3014+/*
3015+jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
3016+Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
3017+*/(function(e){function t(i){var l,h=this,p=i.data||{};if(p.elem)h=i.dragTarget=p.elem,i.dragProxy=a.proxy||h,i.cursorOffsetX=p.pageX-p.left,i.cursorOffsetY=p.pageY-p.top,i.offsetX=i.pageX-i.cursorOffsetX,i.offsetY=i.pageY-i.cursorOffsetY;else if(a.dragging||p.which>0&&i.which!=p.which||e(i.target).is(p.not))return;switch(i.type){case"mousedown":return e.extend(p,e(h).offset(),{elem:h,target:i.target,pageX:i.pageX,pageY:i.pageY}),o.add(document,"mousemove mouseup",t,p),s(h,!1),a.dragging=null,!1;case!a.dragging&&"mousemove":if(r(i.pageX-p.pageX)+r(i.pageY-p.pageY)<p.distance)break;i.target=p.target,l=n(i,"dragstart",h),l!==!1&&(a.dragging=h,a.proxy=i.dragProxy=e(l||h)[0]);case"mousemove":if(a.dragging){if(l=n(i,"drag",h),u.drop&&(u.drop.allowed=l!==!1,u.drop.handler(i)),l!==!1)break;i.type="mouseup"};case"mouseup":o.remove(document,"mousemove mouseup",t),a.dragging&&(u.drop&&u.drop.handler(i),n(i,"dragend",h)),s(h,!0),a.dragging=a.proxy=p.elem=!1}return!0}function n(t,n,r){t.type=n;var i=e.event.dispatch.call(r,t);return i===!1?!1:i||t.result}function r(e){return Math.pow(e,2)}function i(){return a.dragging===!1}function s(e,t){e&&(e.unselectable=t?"off":"on",e.onselectstart=function(){return t},e.style&&(e.style.MozUserSelect=t?"":"none"))}e.fn.drag=function(e,t,n){return t&&this.bind("dragstart",e),n&&this.bind("dragend",n),e?this.bind("drag",t?t:e):this.trigger("drag")};var o=e.event,u=o.special,a=u.drag={not:":input",distance:0,which:1,dragging:!1,setup:function(n){n=e.extend({distance:a.distance,which:a.which,not:a.not},n||{}),n.distance=r(n.distance),o.add(this,"mousedown",t,n),this.attachEvent&&this.attachEvent("ondragstart",i)},teardown:function(){o.remove(this,"mousedown",t),this===a.dragging&&(a.dragging=a.proxy=!1),s(this,!0),this.detachEvent&&this.detachEvent("ondragstart",i)}};u.dragstart=u.dragend={setup:function(){},teardown:function(){}}})(jQuery),function(e){function t(t){var n=t||window.event,r=[].slice.call(arguments,1),i=0,s=0,o=0,t=e.event.fix(n);return t.type="mousewheel",n.wheelDelta&&(i=n.wheelDelta/120),n.detail&&(i=-n.detail/3),o=i,void 0!==n.axis&&n.axis===n.HORIZONTAL_AXIS&&(o=0,s=-1*i),void 0!==n.wheelDeltaY&&(o=n.wheelDeltaY/120),void 0!==n.wheelDeltaX&&(s=-1*n.wheelDeltaX/120),r.unshift(t,i,s,o),(e.event.dispatch||e.event.handle).apply(this,r)}var n=["DOMMouseScroll","mousewheel"];if(e.event.fixHooks)for(var r=n.length;r;)e.event.fixHooks[n[--r]]=e.event.mouseHooks;e.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var e=n.length;e;)this.addEventListener(n[--e],t,!1);else this.onmousewheel=t},teardown:function(){if(this.removeEventListener)for(var e=n.length;e;)this.removeEventListener(n[--e],t,!1);else this.onmousewheel=null}},e.fn.extend({mousewheel:function(e){return e?this.bind("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.unbind("mousewheel",e)}})}(jQuery),function(e){function n(t){function n(e,n){var r=t.offset();r.left=e.pageX-r.left,r.top=e.pageY-r.top,n?t.zoomOut({center:r}):t.zoom({center:r})}function r(e,t){return e.preventDefault(),n(e,t<0),!1}function a(e){if(e.which!=1)return!1;var n=t.getPlaceholder().css("cursor");n&&(i=n),t.getPlaceholder().css("cursor",t.getOptions().pan.cursor),s=e.pageX,o=e.pageY}function f(e){var n=t.getOptions().pan.frameRate;if(u||!n)return;u=setTimeout(function(){t.pan({left:s-e.pageX,top:o-e.pageY}),s=e.pageX,o=e.pageY,u=null},1/n*1e3)}function l(e){u&&(clearTimeout(u),u=null),t.getPlaceholder().css("cursor",i),t.pan({left:s-e.pageX,top:o-e.pageY})}function c(e,t){var i=e.getOptions();i.zoom.interactive&&(t[i.zoom.trigger](n),t.mousewheel(r)),i.pan.interactive&&(t.bind("dragstart",{distance:10},a),t.bind("drag",f),t.bind("dragend",l))}function h(e,t){t.unbind(e.getOptions().zoom.trigger,n),t.unbind("mousewheel",r),t.unbind("dragstart",a),t.unbind("drag",f),t.unbind("dragend",l),u&&clearTimeout(u)}var i="default",s=0,o=0,u=null;t.zoomOut=function(e){e||(e={}),e.amount||(e.amount=t.getOptions().zoom.amount),e.amount=1/e.amount,t.zoom(e)},t.zoom=function(n){n||(n={});var r=n.center,i=n.amount||t.getOptions().zoom.amount,s=t.width(),o=t.height();r||(r={left:s/2,top:o/2});var u=r.left/s,a=r.top/o,f={x:{min:r.left-u*s/i,max:r.left+(1-u)*s/i},y:{min:r.top-a*o/i,max:r.top+(1-a)*o/i}};e.each(t.getAxes(),function(e,t){var n=t.options,r=f[t.direction].min,i=f[t.direction].max,s=n.zoomRange,o=n.panRange;if(s===!1)return;r=t.c2p(r),i=t.c2p(i);if(r>i){var u=r;r=i,i=u}o&&(o[0]!=null&&r<o[0]&&(r=o[0]),o[1]!=null&&i>o[1]&&(i=o[1]));var a=i-r;if(s&&(s[0]!=null&&a<s[0]||s[1]!=null&&a>s[1]))return;n.min=r,n.max=i}),t.setupGrid(),t.draw(),n.preventEvent||t.getPlaceholder().trigger("plotzoom",[t,n])},t.pan=function(n){var r={x:+n.left,y:+n.top};isNaN(r.x)&&(r.x=0),isNaN(r.y)&&(r.y=0),e.each(t.getAxes(),function(e,t){var n=t.options,i,s,o=r[t.direction];i=t.c2p(t.p2c(t.min)+o),s=t.c2p(t.p2c(t.max)+o);var u=n.panRange;if(u===!1)return;u&&(u[0]!=null&&u[0]>i&&(o=u[0]-i,i+=o,s+=o),u[1]!=null&&u[1]<s&&(o=u[1]-s,i+=o,s+=o)),n.min=i,n.max=s}),t.setupGrid(),t.draw(),n.preventEvent||t.getPlaceholder().trigger("plotpan",[t,n])},t.hooks.bindEvents.push(c),t.hooks.shutdown.push(h)}var t={xaxis:{zoomRange:null,panRange:null},zoom:{interactive:!1,trigger:"dblclick",amount:1.5},pan:{interactive:!1,cursor:"move",frameRate:20}};e.plot.plugins.push({init:n,options:t,name:"navigate",version:"1.3"})}(jQuery);
3018\ No newline at end of file
3019
3020=== added file 'dashboard_app/static/dashboard_app/js/jquery.flot.selection.min.js.OTHER'
3021--- dashboard_app/static/dashboard_app/js/jquery.flot.selection.min.js.OTHER 1970-01-01 00:00:00 +0000
3022+++ dashboard_app/static/dashboard_app/js/jquery.flot.selection.min.js.OTHER 2013-09-23 10:56:27 +0000
3023@@ -0,0 +1,79 @@
3024+/* Flot plugin for selecting regions of a plot.
3025+
3026+Copyright (c) 2007-2013 IOLA and Ole Laursen.
3027+Licensed under the MIT license.
3028+
3029+The plugin supports these options:
3030+
3031+selection: {
3032+ mode: null or "x" or "y" or "xy",
3033+ color: color,
3034+ shape: "round" or "miter" or "bevel",
3035+ minSize: number of pixels
3036+}
3037+
3038+Selection support is enabled by setting the mode to one of "x", "y" or "xy".
3039+In "x" mode, the user will only be able to specify the x range, similarly for
3040+"y" mode. For "xy", the selection becomes a rectangle where both ranges can be
3041+specified. "color" is color of the selection (if you need to change the color
3042+later on, you can get to it with plot.getOptions().selection.color). "shape"
3043+is the shape of the corners of the selection.
3044+
3045+"minSize" is the minimum size a selection can be in pixels. This value can
3046+be customized to determine the smallest size a selection can be and still
3047+have the selection rectangle be displayed. When customizing this value, the
3048+fact that it refers to pixels, not axis units must be taken into account.
3049+Thus, for example, if there is a bar graph in time mode with BarWidth set to 1
3050+minute, setting "minSize" to 1 will not make the minimum selection size 1
3051+minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent
3052+"plotunselected" events from being fired when the user clicks the mouse without
3053+dragging.
3054+
3055+When selection support is enabled, a "plotselected" event will be emitted on
3056+the DOM element you passed into the plot function. The event handler gets a
3057+parameter with the ranges selected on the axes, like this:
3058+
3059+ placeholder.bind( "plotselected", function( event, ranges ) {
3060+ alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
3061+ // similar for yaxis - with multiple axes, the extra ones are in
3062+ // x2axis, x3axis, ...
3063+ });
3064+
3065+The "plotselected" event is only fired when the user has finished making the
3066+selection. A "plotselecting" event is fired during the process with the same
3067+parameters as the "plotselected" event, in case you want to know what's
3068+happening while it's happening,
3069+
3070+A "plotunselected" event with no arguments is emitted when the user clicks the
3071+mouse to remove the selection. As stated above, setting "minSize" to 0 will
3072+destroy this behavior.
3073+
3074+The plugin allso adds the following methods to the plot object:
3075+
3076+- setSelection( ranges, preventEvent )
3077+
3078+ Set the selection rectangle. The passed in ranges is on the same form as
3079+ returned in the "plotselected" event. If the selection mode is "x", you
3080+ should put in either an xaxis range, if the mode is "y" you need to put in
3081+ an yaxis range and both xaxis and yaxis if the selection mode is "xy", like
3082+ this:
3083+
3084+ setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
3085+
3086+ setSelection will trigger the "plotselected" event when called. If you don't
3087+ want that to happen, e.g. if you're inside a "plotselected" handler, pass
3088+ true as the second parameter. If you are using multiple axes, you can
3089+ specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of
3090+ xaxis, the plugin picks the first one it sees.
3091+
3092+- clearSelection( preventEvent )
3093+
3094+ Clear the selection rectangle. Pass in true to avoid getting a
3095+ "plotunselected" event.
3096+
3097+- getSelection()
3098+
3099+ Returns the current selection in the same format as the "plotselected"
3100+ event. If there's currently no selection, the function returns null.
3101+
3102+*/(function(e){function t(t){function s(e){n.active&&(h(e),t.getPlaceholder().trigger("plotselecting",[a()]))}function o(t){if(t.which!=1)return;document.body.focus(),document.onselectstart!==undefined&&r.onselectstart==null&&(r.onselectstart=document.onselectstart,document.onselectstart=function(){return!1}),document.ondrag!==undefined&&r.ondrag==null&&(r.ondrag=document.ondrag,document.ondrag=function(){return!1}),c(n.first,t),n.active=!0,i=function(e){u(e)},e(document).one("mouseup",i)}function u(e){return i=null,document.onselectstart!==undefined&&(document.onselectstart=r.onselectstart),document.ondrag!==undefined&&(document.ondrag=r.ondrag),n.active=!1,h(e),m()?f():(t.getPlaceholder().trigger("plotunselected",[]),t.getPlaceholder().trigger("plotselecting",[null])),!1}function a(){if(!m())return null;if(!n.show)return null;var r={},i=n.first,s=n.second;return e.each(t.getAxes(),function(e,t){if(t.used){var n=t.c2p(i[t.direction]),o=t.c2p(s[t.direction]);r[e]={from:Math.min(n,o),to:Math.max(n,o)}}}),r}function f(){var e=a();t.getPlaceholder().trigger("plotselected",[e]),e.xaxis&&e.yaxis&&t.getPlaceholder().trigger("selected",[{x1:e.xaxis.from,y1:e.yaxis.from,x2:e.xaxis.to,y2:e.yaxis.to}])}function l(e,t,n){return t<e?e:t>n?n:t}function c(e,r){var i=t.getOptions(),s=t.getPlaceholder().offset(),o=t.getPlotOffset();e.x=l(0,r.pageX-s.left-o.left,t.width()),e.y=l(0,r.pageY-s.top-o.top,t.height()),i.selection.mode=="y"&&(e.x=e==n.first?0:t.width()),i.selection.mode=="x"&&(e.y=e==n.first?0:t.height())}function h(e){if(e.pageX==null)return;c(n.second,e),m()?(n.show=!0,t.triggerRedrawOverlay()):p(!0)}function p(e){n.show&&(n.show=!1,t.triggerRedrawOverlay(),e||t.getPlaceholder().trigger("plotunselected",[]))}function d(e,n){var r,i,s,o,u=t.getAxes();for(var a in u){r=u[a];if(r.direction==n){o=n+r.n+"axis",!e[o]&&r.n==1&&(o=n+"axis");if(e[o]){i=e[o].from,s=e[o].to;break}}}e[o]||(r=n=="x"?t.getXAxes()[0]:t.getYAxes()[0],i=e[n+"1"],s=e[n+"2"]);if(i!=null&&s!=null&&i>s){var f=i;i=s,s=f}return{from:i,to:s,axis:r}}function v(e,r){var i,s,o=t.getOptions();o.selection.mode=="y"?(n.first.x=0,n.second.x=t.width()):(s=d(e,"x"),n.first.x=s.axis.p2c(s.from),n.second.x=s.axis.p2c(s.to)),o.selection.mode=="x"?(n.first.y=0,n.second.y=t.height()):(s=d(e,"y"),n.first.y=s.axis.p2c(s.from),n.second.y=s.axis.p2c(s.to)),n.show=!0,t.triggerRedrawOverlay(),!r&&m()&&f()}function m(){var e=t.getOptions().selection.minSize;return Math.abs(n.second.x-n.first.x)>=e&&Math.abs(n.second.y-n.first.y)>=e}var n={first:{x:-1,y:-1},second:{x:-1,y:-1},show:!1,active:!1},r={},i=null;t.clearSelection=p,t.setSelection=v,t.getSelection=a,t.hooks.bindEvents.push(function(e,t){var n=e.getOptions();n.selection.mode!=null&&(t.mousemove(s),t.mousedown(o))}),t.hooks.drawOverlay.push(function(t,r){if(n.show&&m()){var i=t.getPlotOffset(),s=t.getOptions();r.save(),r.translate(i.left,i.top);var o=e.color.parse(s.selection.color);r.strokeStyle=o.scale("a",.8).toString(),r.lineWidth=1,r.lineJoin=s.selection.shape,r.fillStyle=o.scale("a",.4).toString();var u=Math.min(n.first.x,n.second.x)+.5,a=Math.min(n.first.y,n.second.y)+.5,f=Math.abs(n.second.x-n.first.x)-1,l=Math.abs(n.second.y-n.first.y)-1;r.fillRect(u,a,f,l),r.strokeRect(u,a,f,l),r.restore()}}),t.hooks.shutdown.push(function(t,n){n.unbind("mousemove",s),n.unbind("mousedown",o),i&&e(document).unbind("mouseup",i)})}e.plot.plugins.push({init:t,options:{selection:{mode:null,color:"#e8cfac",shape:"round",minSize:5}},name:"selection",version:"1.1"})})(jQuery);
3103\ No newline at end of file
3104
3105=== added file 'dashboard_app/static/dashboard_app/js/jquery.flot.stack.min.js.OTHER'
3106--- dashboard_app/static/dashboard_app/js/jquery.flot.stack.min.js.OTHER 1970-01-01 00:00:00 +0000
3107+++ dashboard_app/static/dashboard_app/js/jquery.flot.stack.min.js.OTHER 2013-09-23 10:56:27 +0000
3108@@ -0,0 +1,36 @@
3109+/* Flot plugin for stacking data sets rather than overlyaing them.
3110+
3111+Copyright (c) 2007-2013 IOLA and Ole Laursen.
3112+Licensed under the MIT license.
3113+
3114+The plugin assumes the data is sorted on x (or y if stacking horizontally).
3115+For line charts, it is assumed that if a line has an undefined gap (from a
3116+null point), then the line above it should have the same gap - insert zeros
3117+instead of "null" if you want another behaviour. This also holds for the start
3118+and end of the chart. Note that stacking a mix of positive and negative values
3119+in most instances doesn't make sense (so it looks weird).
3120+
3121+Two or more series are stacked when their "stack" attribute is set to the same
3122+key (which can be any number or string or just "true"). To specify the default
3123+stack, you can set the stack option like this:
3124+
3125+ series: {
3126+ stack: null/false, true, or a key (number/string)
3127+ }
3128+
3129+You can also specify it for a single series, like this:
3130+
3131+ $.plot( $("#placeholder"), [{
3132+ data: [ ... ],
3133+ stack: true
3134+ }])
3135+
3136+The stacking order is determined by the order of the data series in the array
3137+(later series end up on top of the previous).
3138+
3139+Internally, the plugin modifies the datapoints in each series, adding an
3140+offset to the y value. For line series, extra data points are inserted through
3141+interpolation. If there's a second y value, it's also adjusted (e.g for bar
3142+charts or filled areas).
3143+
3144+*/(function(e){function n(e){function t(e,t){var n=null;for(var r=0;r<t.length;++r){if(e==t[r])break;t[r].stack==e.stack&&(n=t[r])}return n}function n(e,n,r){if(n.stack==null||n.stack===!1)return;var i=t(n,e.getData());if(!i)return;var s=r.pointsize,o=r.points,u=i.datapoints.pointsize,a=i.datapoints.points,f=[],l,c,h,p,d,v,m=n.lines.show,g=n.bars.horizontal,y=s>2&&(g?r.format[2].x:r.format[2].y),b=m&&n.lines.steps,w=!0,E=g?1:0,S=g?0:1,x=0,T=0,N,C;for(;;){if(x>=o.length)break;N=f.length;if(o[x]==null){for(C=0;C<s;++C)f.push(o[x+C]);x+=s}else if(T>=a.length){if(!m)for(C=0;C<s;++C)f.push(o[x+C]);x+=s}else if(a[T]==null){for(C=0;C<s;++C)f.push(null);w=!0,T+=u}else{l=o[x+E],c=o[x+S],p=a[T+E],d=a[T+S],v=0;if(l==p){for(C=0;C<s;++C)f.push(o[x+C]);f[N+S]+=d,v=d,x+=s,T+=u}else if(l>p){if(m&&x>0&&o[x-s]!=null){h=c+(o[x-s+S]-c)*(p-l)/(o[x-s+E]-l),f.push(p),f.push(h+d);for(C=2;C<s;++C)f.push(o[x+C]);v=d}T+=u}else{if(w&&m){x+=s;continue}for(C=0;C<s;++C)f.push(o[x+C]);m&&T>0&&a[T-u]!=null&&(v=d+(a[T-u+S]-d)*(l-p)/(a[T-u+E]-p)),f[N+S]+=v,x+=s}w=!1,N!=f.length&&y&&(f[N+2]+=v)}if(b&&N!=f.length&&N>0&&f[N]!=null&&f[N]!=f[N-s]&&f[N+1]!=f[N-s+1]){for(C=0;C<s;++C)f[N+s+C]=f[N+C];f[N+1]=f[N-s+1]}}r.points=f}e.hooks.processDatapoints.push(n)}var t={series:{stack:null}};e.plot.plugins.push({init:n,options:t,name:"stack",version:"1.2"})})(jQuery);
3145\ No newline at end of file
3146
3147=== added directory 'dashboard_app/templates'
3148=== added directory 'dashboard_app/templates/dashboard_app'
3149=== added file 'dashboard_app/templates/dashboard_app/image_report_chart_detail.html.OTHER'
3150--- dashboard_app/templates/dashboard_app/image_report_chart_detail.html.OTHER 1970-01-01 00:00:00 +0000
3151+++ dashboard_app/templates/dashboard_app/image_report_chart_detail.html.OTHER 2013-09-23 10:56:27 +0000
3152@@ -0,0 +1,89 @@
3153+{% extends "dashboard_app/_content.html" %}
3154+{% load i18n %}
3155+
3156+{% block extrahead %}
3157+{{ block.super }}
3158+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/image-charts.css"/>
3159+{% endblock %}
3160+
3161+{% block content %}
3162+
3163+<h1>Image Chart {{ image_chart.name }}</h1>
3164+
3165+
3166+<div class="fields-container">
3167+ <div class="form-field">
3168+ <a href="{{ image_chart.get_absolute_url }}/+edit">Edit</a> this chart.
3169+ </div>
3170+ <div class="form-field">
3171+ Description: {{ image_chart.description }}
3172+ </div>
3173+ <div class="form-field">
3174+ Chart type: {{ image_chart.chart_type }}
3175+ </div>
3176+ <div class="form-field">
3177+ Data table visible: {{ image_chart.is_data_table_visible }}
3178+ </div>
3179+ <div class="form-field">
3180+ Interactive: {{ image_chart.is_interactive }}
3181+ </div>
3182+ <div class="form-field">
3183+ Target goal: {{ image_chart.target_goal|floatformat:"-2" }}
3184+ </div>
3185+</div>
3186+
3187+
3188+<h3>Filters</h3>
3189+
3190+<div class="fields-container">
3191+ <div id="add_filter_link">
3192+ <a href="{{ image_chart.get_absolute_url }}/+add-filter">Add filter</a>
3193+ </div>
3194+</div>
3195+
3196+<div class="list-container">
3197+ {% for chart_filter in image_chart.imagechartfilter_set.all %}
3198+ <div class="chart-title">
3199+ {{ chart_filter.filter.name }}&nbsp;&nbsp;&nbsp;&nbsp;
3200+ <a style="font-size: 13px;" href="{{ chart_filter.get_absolute_url }}">
3201+ edit
3202+ </a>&nbsp;
3203+ <a style="font-size: 13px;" href="{{ chart_filter.get_absolute_url }}/+delete">
3204+ remove
3205+ </a>
3206+ </div>
3207+ <div>
3208+ {% if image_chart.chart_type == "pass/fail" %}
3209+ Tests:&nbsp;
3210+ {% for chart_test in chart_filter.imagecharttest_set.all %}
3211+ {% if forloop.last %}
3212+ {{ chart_test.test.test_id }}
3213+ {% else %}
3214+ {{ chart_test.test.test_id }},&nbsp;
3215+ {% endif %}
3216+ {% endfor %}
3217+ {% else %}
3218+ Test Cases:&nbsp
3219+ {% for chart_test in chart_filter.imagecharttestcase_set.all %}
3220+ {% if forloop.last %}
3221+ {{ chart_test.test_case.test_case_id }}
3222+ {% else %}
3223+ {{ chart_test.test_case.test_case_id }},&nbsp;
3224+ {% endif %}
3225+ {% endfor %}
3226+ {% endif %}
3227+ </div>
3228+ <div>
3229+ Representation: {{ chart_filter.representation }}
3230+ </div>
3231+
3232+ <hr/>
3233+ {% empty %}
3234+ <div>
3235+ <li>No filters added yet.</li>
3236+ </div>
3237+ {% endfor %}
3238+</div>
3239+
3240+
3241+{% endblock %}
3242
3243=== added file 'dashboard_app/templates/dashboard_app/image_report_chart_form.html.OTHER'
3244--- dashboard_app/templates/dashboard_app/image_report_chart_form.html.OTHER 1970-01-01 00:00:00 +0000
3245+++ dashboard_app/templates/dashboard_app/image_report_chart_form.html.OTHER 2013-09-23 10:56:27 +0000
3246@@ -0,0 +1,66 @@
3247+{% extends "dashboard_app/_content.html" %}
3248+{% load i18n %}
3249+{% load django_tables2 %}
3250+
3251+{% block extrahead %}
3252+{{ block.super }}
3253+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/image-charts.css"/>
3254+
3255+{% endblock %}
3256+
3257+
3258+{% block content %}
3259+<h1>Add Image Charts 2.0</h1>
3260+
3261+{% block content_form %}
3262+<form action="" method="post">{% csrf_token %}
3263+
3264+ {% if form.errors %}
3265+ <div class="errors">
3266+ <div>
3267+ {{ form.non_field_errors }}
3268+ <ul>
3269+ {% for field in form %}
3270+ {% if field.errors %}
3271+ <li>{{ field.label }}: {{ field.errors|striptags }}</li>
3272+ {% endif %}
3273+ {% endfor %}
3274+ </ul>
3275+ </div>
3276+ </div>
3277+ {% endif %}
3278+
3279+ <div class="form-field">
3280+ {{ form.name.label_tag }}
3281+ {{ form.name }}
3282+ <input type="hidden" id="id_image_report" name="image_report" value="{{ image_report_id }}"/>
3283+ </div>
3284+ <div class="form-field">
3285+ {{ form.description.label_tag }}
3286+ {{ form.description }}
3287+ </div>
3288+ <div class="form-field">
3289+ {{ form.chart_type.label_tag }}
3290+ {{ form.chart_type }}
3291+ </div>
3292+ <div class="form-field">
3293+ {{ form.is_data_table_visible.label_tag }}
3294+ {{ form.is_data_table_visible }}
3295+ </div>
3296+ <div class="form-field">
3297+ {{ form.is_interactive.label_tag }}
3298+ {{ form.is_interactive }}
3299+ </div>
3300+ <div class="form-field">
3301+ {{ form.target_goal.label_tag }}
3302+ {{ form.target_goal }}
3303+ </div>
3304+
3305+ <div class="submit-button">
3306+ <input type="submit" value="Save" />
3307+ </div>
3308+</form>
3309+
3310+{% endblock content_form %}
3311+
3312+{% endblock %}
3313
3314=== added file 'dashboard_app/templates/dashboard_app/image_report_detail.html.OTHER'
3315--- dashboard_app/templates/dashboard_app/image_report_detail.html.OTHER 1970-01-01 00:00:00 +0000
3316+++ dashboard_app/templates/dashboard_app/image_report_detail.html.OTHER 2013-09-23 10:56:27 +0000
3317@@ -0,0 +1,77 @@
3318+{% extends "dashboard_app/_content.html" %}
3319+{% load i18n %}
3320+
3321+{% block extrahead %}
3322+{{ block.super }}
3323+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/image-charts.css"/>
3324+{% endblock %}
3325+
3326+{% block content %}
3327+
3328+<h1>Image Report {{ image_report.name }}</h1>
3329+
3330+<div class="fields-container">
3331+ <div class="form-field">
3332+ Status&#58;&nbsp;
3333+ {% if image_report.is_published %}
3334+ <span style="font-weight: bold; color: green;">
3335+ Published
3336+ </span>
3337+ {% else %}
3338+ <span style="font-weight: bold; color: orange;">
3339+ Not Published
3340+ </span>
3341+ {% endif %}
3342+ </div>
3343+ <div class="form-field">
3344+ Description&#58;&nbsp;{{ image_report.description }}
3345+ </div>
3346+ <div class="form-field">
3347+ <a href="{{ image_report.get_absolute_url }}/+edit">Edit</a> this image report.
3348+ </div>
3349+ <div class="form-field">
3350+ <a href="{{ image_report.get_absolute_url }}">Preview</a> this image report.
3351+ </div>
3352+ <div class="form-field">
3353+ {% if image_report.is_published %}
3354+ <a href="{{ image_report.get_absolute_url }}/+unpublish">Unpublish</a> this image report.
3355+ {% else %}
3356+ <a href="{{ image_report.get_absolute_url }}/+publish">Publish</a> this image report.
3357+{% endif %}
3358+ </div>
3359+</div>
3360+
3361+<h3>Charts</h3>
3362+
3363+<div class="fields-container">
3364+ <a href="{% url dashboard_app.views.image_reports.views.image_chart_add %}?image_report_id={{ image_report.id }}">
3365+ Add new chart
3366+ </a>
3367+</div>
3368+
3369+<div class="list-container">
3370+ {% for image_chart in image_report.imagereportchart_set.all %}
3371+ <div class="chart-title">
3372+ {{ image_chart.name }}&nbsp;&nbsp;
3373+ <a style="font-size: 13px;" href="{{ image_chart.get_absolute_url }}">
3374+ details
3375+ </a>&nbsp;
3376+ </div>
3377+ <div>
3378+ Description: {{ image_chart.description }}
3379+ </div>
3380+ <div>
3381+ Chart type: {{ image_chart.chart_type }}
3382+ </div>
3383+ <div>
3384+ Target goal: {{ image_chart.target_goal|floatformat:"-2" }}
3385+ </div>
3386+ <hr/>
3387+ {% empty %}
3388+ <div>
3389+ <li>No charts added yet.</li>
3390+ </div>
3391+ {% endfor %}
3392+</div>
3393+
3394+{% endblock %}
3395
3396=== added file 'dashboard_app/templates/dashboard_app/image_report_display.html'
3397--- dashboard_app/templates/dashboard_app/image_report_display.html 1970-01-01 00:00:00 +0000
3398+++ dashboard_app/templates/dashboard_app/image_report_display.html 2013-09-23 10:56:27 +0000
3399@@ -0,0 +1,26 @@
3400+{% extends "dashboard_app/_content.html" %}
3401+{% load i18n %}
3402+
3403+{% block extrahead %}
3404+{{ block.super }}
3405+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/image-charts.css"/>
3406+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/image-chart.js"></script>
3407+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.min.js"></script>
3408+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.canvas.min.js"></script>
3409+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.flot.dashes.min.js"></script>
3410+<script src="{{ STATIC_URL }}dashboard_app/js/jquery.tooltip.min.js"></script>
3411+
3412+<script language="javascript">
3413+ chart_data = $.parseJSON($('<div/>').html('{{chart_data}}').text());
3414+// alert(JSON.stringify(chart_data, null, 4));
3415+</script>
3416+{% endblock %}
3417+
3418+{% block content %}
3419+
3420+<h1>Image Report {{ image_report.name }}</h1>
3421+
3422+<div id="main_container">
3423+</div>
3424+
3425+{% endblock %}
3426
3427=== added file 'dashboard_app/templates/dashboard_app/image_report_list.html.OTHER'
3428--- dashboard_app/templates/dashboard_app/image_report_list.html.OTHER 1970-01-01 00:00:00 +0000
3429+++ dashboard_app/templates/dashboard_app/image_report_list.html.OTHER 2013-09-23 10:56:27 +0000
3430@@ -0,0 +1,40 @@
3431+{% extends "dashboard_app/_content.html" %}
3432+
3433+{% block extrahead %}
3434+{{ block.super }}
3435+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/image-charts.css"/>
3436+{% endblock %}
3437+
3438+{% block content %}
3439+<h1>Image Reports 2.0</h1>
3440+
3441+<p style="margin-left: 10px;">
3442+ <a href="{% url dashboard_app.views.image_reports.views.image_report_add %}">
3443+ Add new Image Report
3444+ </a>
3445+</p>
3446+
3447+{% for image_report in image_reports %}
3448+<div class="list-container">
3449+ <div style="float: left;">
3450+ <a href="{{ image_report.get_absolute_url }}">{{ image_report.name }}</a>
3451+ &nbsp;&nbsp;
3452+ </div>
3453+ {% if image_report.is_published %}
3454+ <div style="font-weight: bold; float: right; color: green;">
3455+ Published
3456+ </div>
3457+ {% else %}
3458+ <div style="font-weight: bold; float: right; color: orange;">
3459+ Not Published
3460+ </div>
3461+ {% endif %}
3462+ <div style="float: right; margin-right: 20px;">
3463+ <a href="{{ image_report.get_absolute_url }}/+detail">details</a>
3464+ </div>
3465+ <div style="clear: both;">
3466+ {{ image_report.description }}
3467+ </div>
3468+</div>
3469+{% endfor %}
3470+{% endblock %}
3471
3472=== added file 'dashboard_app/urls.py.OTHER'
3473--- dashboard_app/urls.py.OTHER 1970-01-01 00:00:00 +0000
3474+++ dashboard_app/urls.py.OTHER 2013-09-23 10:56:27 +0000
3475@@ -0,0 +1,91 @@
3476+# Copyright (C) 2010 Linaro Limited
3477+#
3478+# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
3479+#
3480+# This file is part of Launch Control.
3481+#
3482+# Launch Control is free software: you can redistribute it and/or modify
3483+# it under the terms of the GNU Affero General Public License version 3
3484+# as published by the Free Software Foundation
3485+#
3486+# Launch Control is distributed in the hope that it will be useful,
3487+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3488+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3489+# GNU General Public License for more details.
3490+#
3491+# You should have received a copy of the GNU Affero General Public License
3492+# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
3493+
3494+"""
3495+URL mappings for the Dashboard application
3496+"""
3497+from django.conf.urls.defaults import *
3498+
3499+urlpatterns = patterns(
3500+ 'dashboard_app.views',
3501+ url(r'^$', 'index'),
3502+ url(r'^ajax/bundle-viewer/(?P<pk>[0-9]+)/$', 'ajax_bundle_viewer'),
3503+ url(r'^data-views/$', 'data_view_list'),
3504+ url(r'^data-views/(?P<name>[a-zA-Z0-9-_]+)/$', 'data_view_detail'),
3505+ url(r'^reports/$', 'report_list'),
3506+ url(r'^reports/(?P<name>[a-zA-Z0-9-_]+)/$', 'report_detail'),
3507+ url(r'^filters/$', 'filters.views.filters_list'),
3508+ url(r'^filters/\+add$', 'filters.views.filter_add'),
3509+ url(r'^filters/\+add-preview-json$', 'filters.views.filter_preview_json'),
3510+ url(r'^filters/\+add-cases-for-test-json$', 'filters.views.filter_add_cases_for_test_json'),
3511+ url(r'^filters/\+get-tests-json$', 'filters.views.get_tests_json'),
3512+ url(r'^filters/\+get-test-cases-json$', 'filters.views.get_test_cases_json'),
3513+ url(r'^filters/\+attribute-name-completion-json$', 'filters.views.filter_attr_name_completion_json'),
3514+ url(r'^filters/\+attribute-value-completion-json$', 'filters.views.filter_attr_value_completion_json'),
3515+ url(r'^filters/~(?P<username>[^/]+)/(?P<name>[a-zA-Z0-9-_]+)$', 'filters.views.filter_detail'),
3516+ url(r'^filters/~(?P<username>[^/]+)/(?P<name>[a-zA-Z0-9-_]+)/json$', 'filters.views.filter_json'),
3517+ url(r'^filters/~(?P<username>[^/]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filters.views.filter_edit'),
3518+ url(r'^filters/~(?P<username>[^/]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filters.views.filter_subscribe'),
3519+ url(r'^filters/~(?P<username>[^/]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filters.views.filter_delete'),
3520+ url(r'^filters/~(?P<username>[^/]+)/(?P<name>[a-zA-Z0-9-_]+)/\+compare/(?P<tag1>[a-zA-Z0-9-_: .]+)/(?P<tag2>[a-zA-Z0-9-_: .]+)$', 'filters.views.compare_matches'),
3521+ url(r'^streams/$', 'bundle_stream_list'),
3522+ url(r'^streams/json$', 'bundle_stream_list_json'),
3523+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/$', 'bundle_list'),
3524+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/json$', 'bundle_list_table_json'),
3525+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/$', 'bundle_detail'),
3526+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/json$', 'bundle_json'),
3527+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'test_run_detail'),
3528+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/json$', 'test_run_detail_test_json'),
3529+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)bundles/(?P<content_sha1>[0-9a-z]+)/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/result/(?P<relative_index>[0-9]+)/$', 'test_result_detail'),
3530+ 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'),
3531+ 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'),
3532+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)test-runs/$', 'test_run_list'),
3533+ url(r'^streams(?P<pathname>/[a-zA-Z0-9/._-]+)test-runs/json$', 'test_run_list_json'),
3534+ url(r'^attachment/(?P<pk>[0-9]+)/download$', 'attachment_download'),
3535+ url(r'^attachment/(?P<pk>[0-9]+)/view$', 'attachment_view'),
3536+ url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/$', 'redirect_to_test_run'),
3537+ url(r'^permalink/test-run/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<trailing>.*)$', 'redirect_to_test_run'),
3538+ url(r'^permalink/test-result/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<relative_index>[0-9]+)/$', 'redirect_to_test_result'),
3539+ url(r'^permalink/test-result/(?P<analyzer_assigned_uuid>[a-zA-Z0-9-]+)/(?P<relative_index>[0-9]+)/(?P<trailing>.*)$', 'redirect_to_test_result'),
3540+ url(r'^permalink/bundle/(?P<content_sha1>[0-9a-z]+)/$', 'redirect_to_bundle'),
3541+ url(r'^permalink/bundle/(?P<content_sha1>[0-9a-z]+)/(?P<trailing>.*)$', 'redirect_to_bundle'),
3542+ url(r'^image-reports/$', 'images.image_report_list'),
3543+ url(r'^image-charts/$', 'image_reports.views.image_report_list'),
3544+ url(r'^image-charts/(?P<name>[a-zA-Z0-9-_]+)$', 'image_reports.views.image_report_display'),
3545+ url(r'^image-charts/(?P<name>[a-zA-Z0-9-_]+)/\+detail$', 'image_reports.views.image_report_detail'),
3546+ url(r'^image-charts/\+add$', 'image_reports.views.image_report_add'),
3547+ url(r'^image-charts/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'image_reports.views.image_report_edit'),
3548+ url(r'^image-charts/(?P<name>[a-zA-Z0-9-_]+)/\+publish$', 'image_reports.views.image_report_publish'),
3549+ url(r'^image-charts/(?P<name>[a-zA-Z0-9-_]+)/\+unpublish$', 'image_reports.views.image_report_unpublish'),
3550+ url(r'^image-chart/(?P<id>[a-zA-Z0-9-_]+)$', 'image_reports.views.image_chart_detail'),
3551+ url(r'^image-chart/\+add$', 'image_reports.views.image_chart_add'),
3552+ url(r'^image-chart/(?P<id>[a-zA-Z0-9-_]+)/\+edit$', 'image_reports.views.image_chart_edit'),
3553+ url(r'^image-chart/(?P<id>[a-zA-Z0-9-_]+)/\+add-filter$', 'image_reports.views.image_chart_filter_add'),
3554+ url(r'^image-chart-filter/(?P<id>[a-zA-Z0-9-_]+)$', 'image_reports.views.image_chart_filter_edit'),
3555+ url(r'^image-chart-filter/(?P<id>[a-zA-Z0-9-_]+)/\+delete$', 'image_reports.views.image_chart_filter_delete'),
3556+ url(r'^pmqa$', 'pmqa.pmqa_view'),
3557+ url(r'^pmqa(?P<pathname>/[a-zA-Z0-9/._-]+/)(?P<device_type>[a-zA-Z0-9-_]+)$', 'pmqa.pmqa_filter_view'),
3558+ url(r'^pmqa(?P<pathname>/[a-zA-Z0-9/._-]+/)(?P<device_type>[a-zA-Z0-9-_]+)/json$', 'pmqa.pmqa_filter_view_json'),
3559+ url(r'^pmqa(?P<pathname>/[a-zA-Z0-9/._-]+/)(?P<device_type>[a-zA-Z0-9-_]+)/\+compare/(?P<build1>[0-9]+)/(?P<build2>[0-9]+)$', 'pmqa.compare_pmqa_results'),
3560+ url(r'^image-reports/(?P<name>[A-Za-z0-9_-]+)$', 'images.image_report_detail'),
3561+ url(r'^api/link-bug-to-testrun', 'images.link_bug_to_testrun'),
3562+ url(r'^api/unlink-bug-and-testrun', 'images.unlink_bug_and_testrun'),
3563+ url(r'^test-definition/add_test_definition', 'add_test_definition'),
3564+ url(r'^test-definition/$', 'test_definition'),
3565+ url(r'^testdefinition_table_json$', 'testdefinition_table_json'),
3566+)
3567
3568=== added directory 'dashboard_app/views'
3569=== added directory 'dashboard_app/views/image_reports'
3570=== added file 'dashboard_app/views/image_reports/views.py.OTHER'
3571--- dashboard_app/views/image_reports/views.py.OTHER 1970-01-01 00:00:00 +0000
3572+++ dashboard_app/views/image_reports/views.py.OTHER 2013-09-23 10:56:27 +0000
3573@@ -0,0 +1,341 @@
3574+# Copyright (C) 2010-2013 Linaro Limited
3575+#
3576+# Author: Stevan Radakovic <stevan.radakovic@linaro.org>
3577+#
3578+# This file is part of Launch Control.
3579+#
3580+# Launch Control is free software: you can redistribute it and/or modify
3581+# it under the terms of the GNU Affero General Public License version 3
3582+# as published by the Free Software Foundation
3583+#
3584+# Launch Control is distributed in the hope that it will be useful,
3585+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3586+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3587+# GNU General Public License for more details.
3588+#
3589+# You should have received a copy of the GNU Affero General Public License
3590+# along with Launch Control. If not, see <http://www.gnu.org/licenses/>.
3591+
3592+import simplejson
3593+
3594+from django.contrib.auth.decorators import login_required
3595+from django.core.exceptions import PermissionDenied, ValidationError
3596+from django.http import HttpResponse, HttpResponseRedirect
3597+from django.shortcuts import render_to_response
3598+from django.template import RequestContext
3599+from django.utils.safestring import mark_safe
3600+
3601+from lava_server.bread_crumbs import (
3602+ BreadCrumb,
3603+ BreadCrumbTrail,
3604+)
3605+
3606+from dashboard_app.views import index
3607+
3608+from dashboard_app.views.image_reports.forms import (
3609+ ImageReportEditorForm,
3610+ ImageReportChartForm,
3611+ ImageChartFilterForm,
3612+ )
3613+
3614+from dashboard_app.models import (
3615+ ImageReport,
3616+ ImageReportChart,
3617+ ImageChartFilter,
3618+ ImageChartTest,
3619+ ImageChartTestCase,
3620+ Test,
3621+ TestCase,
3622+ TestRunFilter,
3623+ )
3624+
3625+from dashboard_app.views.filters.tables import AllFiltersSimpleTable
3626+
3627+
3628+
3629+@BreadCrumb("Image reports", parent=index)
3630+def image_report_list(request):
3631+
3632+ if request.user.is_authenticated():
3633+ image_reports = ImageReport.objects.all()
3634+ else:
3635+ image_reports = None
3636+
3637+ return render_to_response(
3638+ 'dashboard_app/image_report_list.html', {
3639+ "image_reports": image_reports,
3640+ }, RequestContext(request)
3641+ )
3642+
3643+@BreadCrumb("Image report {name}", parent=image_report_list, needs=['name'])
3644+def image_report_display(request, name):
3645+ image_report = ImageReport.objects.get(name=name)
3646+ chart_data = {}
3647+ for chart in image_report.imagereportchart_set.all():
3648+ chart_data[chart.name] = chart.get_chart_data(request.user)
3649+
3650+ return render_to_response(
3651+ 'dashboard_app/image_report_display.html', {
3652+ 'image_report': image_report,
3653+ 'chart_data': simplejson.dumps(chart_data),
3654+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
3655+ image_report_detail, name=name),
3656+ }, RequestContext(request)
3657+ )
3658+
3659+@BreadCrumb("Image report {name}", parent=image_report_list, needs=['name'])
3660+def image_report_detail(request, name):
3661+ image_report = ImageReport.objects.get(name=name)
3662+
3663+ return render_to_response(
3664+ 'dashboard_app/image_report_detail.html', {
3665+ 'image_report': image_report,
3666+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
3667+ image_report_detail, name=name),
3668+ }, RequestContext(request)
3669+ )
3670+
3671+@BreadCrumb("Add new image report", parent=image_report_list)
3672+@login_required
3673+def image_report_add(request):
3674+ return image_report_form(
3675+ request,
3676+ BreadCrumbTrail.leading_to(image_report_add))
3677+
3678+@BreadCrumb("Update image report {name}", parent=image_report_list,
3679+ needs=['name'])
3680+@login_required
3681+def image_report_edit(request, name):
3682+ image_report = ImageReport.objects.get(name=name)
3683+ return image_report_form(
3684+ request,
3685+ BreadCrumbTrail.leading_to(image_report_edit,
3686+ name=name),
3687+ instance=image_report)
3688+
3689+@BreadCrumb("Publish image report {name}", parent=image_report_list,
3690+ needs=['name'])
3691+@login_required
3692+def image_report_publish(request, name):
3693+ image_report = ImageReport.objects.get(name=name)
3694+ image_report.is_published = True
3695+ image_report.save()
3696+
3697+ return render_to_response(
3698+ 'dashboard_app/image_report_detail.html', {
3699+ 'image_report': image_report,
3700+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
3701+ image_report_detail, name=name),
3702+ }, RequestContext(request)
3703+ )
3704+
3705+@BreadCrumb("Unpublish image report {name}", parent=image_report_list,
3706+ needs=['name'])
3707+@login_required
3708+def image_report_unpublish(request, name):
3709+ image_report = ImageReport.objects.get(name=name)
3710+ image_report.is_published = False
3711+ image_report.save()
3712+
3713+ return render_to_response(
3714+ 'dashboard_app/image_report_detail.html', {
3715+ 'image_report': image_report,
3716+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
3717+ image_report_detail, name=name),
3718+ }, RequestContext(request)
3719+ )
3720+
3721+def image_report_form(request, bread_crumb_trail, instance=None):
3722+
3723+ if request.method == 'POST':
3724+
3725+ form = ImageReportEditorForm(request.user, request.POST,
3726+ instance=instance)
3727+ if form.is_valid():
3728+ image_report = form.save()
3729+ return HttpResponseRedirect(image_report.get_absolute_url()
3730+ + "/+detail")
3731+
3732+ else:
3733+ form = ImageReportEditorForm(request.user, instance=instance)
3734+
3735+ return render_to_response(
3736+ 'dashboard_app/image_report_form.html', {
3737+ 'bread_crumb_trail': bread_crumb_trail,
3738+ 'form': form,
3739+ }, RequestContext(request))
3740+
3741+@BreadCrumb("Image chart details", parent=image_report_list)
3742+def image_chart_detail(request, id):
3743+ image_chart = ImageReportChart.objects.get(id=id)
3744+
3745+ return render_to_response(
3746+ 'dashboard_app/image_report_chart_detail.html', {
3747+ 'image_chart': image_chart,
3748+ 'bread_crumb_trail': BreadCrumbTrail.leading_to(
3749+ image_chart_detail, id=id),
3750+ }, RequestContext(request)
3751+ )
3752+
3753+@BreadCrumb("Add new image chart", parent=image_report_list)
3754+@login_required
3755+def image_chart_add(request):
3756+ return image_chart_form(
3757+ request,
3758+ BreadCrumbTrail.leading_to(image_chart_add))
3759+
3760+@BreadCrumb("Update image chart", parent=image_report_list)
3761+@login_required
3762+def image_chart_edit(request, id):
3763+ image_chart = ImageReportChart.objects.get(id=id)
3764+ return image_chart_form(
3765+ request,
3766+ BreadCrumbTrail.leading_to(image_chart_edit,
3767+ id=id),
3768+ instance=image_chart)
3769+
3770+def image_chart_form(request, bread_crumb_trail, instance=None):
3771+
3772+ if request.method == 'POST':
3773+
3774+ form = ImageReportChartForm(request.user, request.POST,
3775+ instance=instance)
3776+ if form.is_valid():
3777+ image_chart = form.save()
3778+ return HttpResponseRedirect(
3779+ image_chart.get_absolute_url())
3780+
3781+ else:
3782+ form = ImageReportChartForm(request.user, instance=instance)
3783+
3784+ if not instance:
3785+ image_report_id = request.GET.get('image_report_id', None)
3786+ else:
3787+ image_report_id = instance.image_report.id
3788+
3789+ filters_table = AllFiltersSimpleTable("all-filters", None)
3790+
3791+ return render_to_response(
3792+ 'dashboard_app/image_report_chart_form.html', {
3793+ 'bread_crumb_trail': bread_crumb_trail,
3794+ 'form': form,
3795+ 'filters_table': filters_table,
3796+ 'image_report_id': image_report_id,
3797+ }, RequestContext(request))
3798+
3799+@BreadCrumb("Image chart add filter", parent=image_report_list)
3800+def image_chart_filter_add(request, id):
3801+ image_chart = ImageReportChart.objects.get(id=id)
3802+ return image_chart_filter_form(
3803+ request,
3804+ BreadCrumbTrail.leading_to(image_chart_filter_add),
3805+ chart_instance=image_chart)
3806+
3807+@BreadCrumb("Update image chart filter", parent=image_report_list)
3808+@login_required
3809+def image_chart_filter_edit(request, id):
3810+ image_chart_filter = ImageChartFilter.objects.get(id=id)
3811+ return image_chart_filter_form(
3812+ request,
3813+ BreadCrumbTrail.leading_to(image_chart_filter_edit, id=id),
3814+ instance=image_chart_filter)
3815+
3816+@BreadCrumb("Image chart add filter", parent=image_report_list)
3817+def image_chart_filter_delete(request, id):
3818+ image_chart_filter = ImageChartFilter.objects.get(id=id)
3819+ url = image_chart_filter.image_chart.get_absolute_url()
3820+ image_chart_filter.delete()
3821+ return HttpResponseRedirect(url)
3822+
3823+def image_chart_filter_form(request, bread_crumb_trail, chart_instance=None,
3824+ instance=None):
3825+
3826+ if instance:
3827+ chart_instance = instance.image_chart
3828+
3829+ if request.method == 'POST':
3830+
3831+ form = ImageChartFilterForm(request.user, request.POST,
3832+ instance=instance)
3833+
3834+ if form.is_valid():
3835+
3836+ chart_filter = form.save()
3837+ aliases = request.POST.getlist('aliases')
3838+
3839+ if chart_filter.image_chart.chart_type == 'pass/fail':
3840+
3841+ image_chart_tests = Test.objects.filter(
3842+ imagecharttest__image_chart_filter=chart_filter).order_by(
3843+ 'id')
3844+
3845+ tests = form.cleaned_data['image_chart_tests']
3846+
3847+ for index, test in enumerate(tests):
3848+ if test in image_chart_tests:
3849+ chart_test = ImageChartTest.objects.get(
3850+ image_chart_filter=chart_filter, test=test)
3851+ chart_test.name = aliases[index]
3852+ chart_test.save()
3853+ else:
3854+ chart_test = ImageChartTest()
3855+ chart_test.image_chart_filter = chart_filter
3856+ chart_test.test = test
3857+ chart_test.name = aliases[index]
3858+ chart_test.save()
3859+
3860+ for index, chart_test in enumerate(image_chart_tests):
3861+ if chart_test not in tests:
3862+ ImageChartTest.objects.get(
3863+ image_chart_filter=chart_filter,
3864+ test=chart_test).delete()
3865+
3866+ return HttpResponseRedirect(
3867+ chart_filter.image_chart.get_absolute_url())
3868+
3869+ else:
3870+
3871+ image_chart_test_cases = TestCase.objects.filter(
3872+ imagecharttestcase__image_chart_filter=
3873+ chart_filter).order_by('id')
3874+
3875+ test_cases = form.cleaned_data['image_chart_test_cases']
3876+
3877+ for index, test_case in enumerate(test_cases):
3878+ if test_case in image_chart_test_cases:
3879+ chart_test_case = ImageChartTestCase.objects.get(
3880+ image_chart_filter=chart_filter,
3881+ test_case=test_case)
3882+ chart_test_case.name = aliases[index]
3883+ chart_test_case.save()
3884+ else:
3885+ chart_test_case = ImageChartTestCase()
3886+ chart_test_case.image_chart_filter = chart_filter
3887+ chart_test_case.test_case = test_case
3888+ chart_test_case.name = aliases[index]
3889+ chart_test_case.save()
3890+
3891+ for index, chart_test_case in enumerate(
3892+ image_chart_test_cases):
3893+ if chart_test_case not in test_cases:
3894+ ImageChartTestCase.objects.get(
3895+ image_chart_filter=chart_filter,
3896+ test_case=chart_test_case).delete()
3897+
3898+ return HttpResponseRedirect(
3899+ chart_filter.image_chart.get_absolute_url())
3900+
3901+ else:
3902+ form = ImageChartFilterForm(request.user, instance=instance,
3903+ initial={'image_chart': chart_instance})
3904+
3905+ filters_table = AllFiltersSimpleTable("all-filters", None)
3906+
3907+ return render_to_response(
3908+ 'dashboard_app/image_chart_filter_form.html', {
3909+ 'bread_crumb_trail': bread_crumb_trail,
3910+ 'filters_table': filters_table,
3911+ 'image_chart': chart_instance,
3912+ 'instance': instance,
3913+ 'form': form,
3914+ }, RequestContext(request))

Subscribers

People subscribed via source and target branches