Merge lp:~stevanr/lava-dashboard/image-reports-gui into lp:lava-dashboard
- image-reports-gui
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stevan Radaković | Disapprove | ||
Review via email: mp+187003@code.launchpad.net |
Commit message
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,"&").replace(/"/g,""")}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: </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: </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> End build number: </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> </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 }} |
3200 | + <a style="font-size: 13px;" href="{{ chart_filter.get_absolute_url }}"> |
3201 | + edit |
3202 | + </a> |
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: |
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 }}, |
3215 | + {% endif %} |
3216 | + {% endfor %} |
3217 | + {% else %} |
3218 | + Test Cases:  |
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 }}, |
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: |
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: {{ 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 }} |
3373 | + <a style="font-size: 13px;" href="{{ image_chart.get_absolute_url }}"> |
3374 | + details |
3375 | + </a> |
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 | + |
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)) |