Merge lp:~canonical-ci-engineering/uci-engine/nfss into lp:uci-engine

Proposed by Thomi Richards on 2014-06-28
Status: Merged
Approved by: Francis Ginther on 2014-07-01
Approved revision: 647
Merged at revision: 636
Proposed branch: lp:~canonical-ci-engineering/uci-engine/nfss
Merge into: lp:uci-engine
Diff against target: 2501 lines (+2336/-4)
26 files modified
charms/precise/restish/hooks/hooks.py (+1/-1)
juju-deployer/configs/nfss_http_vhost (+19/-0)
juju-deployer/deploy.py (+3/-3)
juju-deployer/nf-stats-service.yaml.tmpl (+51/-0)
nf-stats-service/README (+37/-0)
nf-stats-service/clean_db.sh (+6/-0)
nf-stats-service/db_patches/000-initial_schema.sql (+94/-0)
nf-stats-service/nfss/__init__.py (+55/-0)
nf-stats-service/nfss/__main__.py (+220/-0)
nf-stats-service/nfss/api/__init__.py (+14/-0)
nf-stats-service/nfss/api/v1.py (+140/-0)
nf-stats-service/nfss/auth.py (+108/-0)
nf-stats-service/nfss/database.py (+454/-0)
nf-stats-service/nfss/db_migrate.py (+162/-0)
nf-stats-service/nfss/tests/unit/__init__.py (+14/-0)
nf-stats-service/nfss/tests/unit/test_db_migration.py (+93/-0)
nf-stats-service/setup.py (+32/-0)
nf-stats-service/web_static/app.js (+243/-0)
nf-stats-service/web_static/graphs/app_startup_benchmark.html (+61/-0)
nf-stats-service/web_static/graphs/bootspeed.html (+66/-0)
nf-stats-service/web_static/index.html (+73/-0)
nf-stats-service/web_static/style.css (+192/-0)
nf-stats-service/web_static/v/angularjs-nvd3-directives.min.js (+3/-0)
nf-stats-service/web_static/v/ng-quick-date-default-theme.css (+104/-0)
nf-stats-service/web_static/v/ng-quick-date.css (+90/-0)
nf-stats-service/web_static/v/ng-quick-date.min.js (+1/-0)
To merge this branch: bzr merge lp:~canonical-ci-engineering/uci-engine/nfss
Reviewer Review Type Date Requested Status
Francis Ginther 2014-06-28 Approve on 2014-07-01
PS Jenkins bot (community) continuous-integration Approve on 2014-07-01
Andy Doan (community) Approve on 2014-07-01
Review via email: mp+224908@code.launchpad.net

Commit message

Initial import of non functional stats service app.

Description of the change

This branch contains the non functional stats service, due to be deployed to a server near you soon.

To post a comment you must log in.
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:642
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/963/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/963/rebuild

review: Approve (continuous-integration)
Andy Doan (doanac) wrote :

I have some comments and some notes to help explain things in case other people review this.

I have no business trying to review the j/s angular stuff.

My only other concern is the directory, nf-stats-service/web_static/v. I'd prefer to merge this without carrying that along. I'd give bonus points if our merge history didn't include it, but that might be too painful to deal with. VCS is forever, so each little 500k mishap in an MP can start to add up over time.

643. By Thomi Richards on 2014-06-30

Turn ProxyPreserveHost on so oauth authentication works correctly.

644. By Thomi Richards on 2014-06-30

Fixed up issues from code review.

Hi Andy,

Thanks for the review. I've either fixed all the issues you found, or have added a note to the diff.

Cheers,

PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:644
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/973/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/973/rebuild

review: Approve (continuous-integration)
Francis Ginther (fginther) wrote :

This new service looks reasonable and I don't see anything that would prevent future integration into the uci-engine deployment. A few things to address in the future:
 - The api will need to be available via it's own namespace. So instead of an accessing it externally as /api/v1/foo, it would be /nfss/api/v1/foo. This can be done through the apache rewrite rules, so not a concern now.
 - Need to integrate with the uci-engine test infrastructure.
 - It would be nice to host the web-static content, but I understand that this is specific to this version of the deployment and can see why it would be appropriate to have this in the source tree (just like the charms are in the source tree).

I had one question regarding juju-deployer/nf-stats-service.yaml.tmpl in the inline comments.

review: Needs Information
Francis Ginther (fginther) wrote :

> - It would be nice to host the web-static content,
I didn't finish this thought...

It would be nice to host the web-static content in an external location. One of our goals for the charms in the source tree is to eventually host them in their own branch when they are determined to be stable enough.

Joe Talbott (joetalbott) wrote :

Unrelated to actually reviewing the MP.

We could, and probably should, create our own static content service app.

Joe Talbott (joetalbott) wrote :

The apache bits look good to me. I don't see any issues with including this app in the ci-airline.

One minor comment inline. I think moving your global variables between the imports section and the function definitions would be better.

Andy Doan (doanac) wrote :

responded to fginther's question.

Andy Doan (doanac) wrote :

On 06/30/2014 06:36 PM, Thomi Richards wrote:
>> + @unittest.skipUnless(have_mock, "No Mock available")
> When this gets deployed, we don't have mock installed, but pyramid scans the test packages to make the wsgi application. For that reason, packages need to be importable even if the test dependencies aren't available.

I thought mock is a part of python3? ie:

  python3 -c "import mock"

>> >=== added file 'nf-stats-service/setup.py'

>> >+requires = [
>> >+ 'mock==1.0.1',
> See note above...

If we do need this, we'll eventually have to include the actual
dependency in:

   lp:~canonical-ci-engineering/uci-engine/deps

once we figure out how to integrate this with tarmac testing.

645. By Thomi Richards on 2014-07-01

Added test dependencies, removed mock since it's included in py34.

646. By Thomi Richards on 2014-07-01

Add oauthlib test dep.

PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:645
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/987/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/987/rebuild

review: Approve (continuous-integration)
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:646
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/988/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/988/rebuild

review: Approve (continuous-integration)

Various replies to various people:

Francis: As discussed on IRC, the fact that we're using python 3 means that integrating with the run-tests command isn't possible right now. However, I have made sure that doing 'python setup.py test' installs all the test dependencies (although there's a problem on some machines with the psycopg2 package from pypi).

WRT hosting static content separately, I'd like it to remain in the same source tree. Beyond that, I don't have anything intelligent to say on the topic. Please advise if you want any changes made.

WRT moving the API to be /nfss/api/v1 - surely that will require changes to the client-side UI as well? It's trivial to change the routing rules in pyramid (it's literally a 1 line change in __init__.py), I just want to make sure that we don't break things down the line.

Andy: The mock dependency has been removed - thanks for pointing out that it exists in python 3.4.

Andy Doan (doanac) wrote :

On 07/01/2014 05:13 PM, Thomi Richards wrote:
> WRT moving the API to be /nfss/api/v1 - surely that will require changes to the client-side UI as well? It's trivial to change the routing rules in pyramid (it's literally a 1 line change in __init__.py), I just want to make sure that we don't break things down the line.

I'm not sure this is needed. It certainly won't require changes to
pyramid. The question is really this: do we plan to host the static
content on ci-airline.ubuntu.com? I wouldn't think so. I'd think this
service would have its own public apache like we currently use and it
would have its own DNS name?

Andy Doan (doanac) wrote :

my comments are addressed

review: Approve

On Wed, Jul 2, 2014 at 10:18 AM, Andy Doan <email address hidden>
wrote:

>
> I'm not sure this is needed. It certainly won't require changes to
> pyramid. The question is really this: do we plan to host the static
> content on ci-airline.ubuntu.com? I wouldn't think so. I'd think this
> service would have its own public apache like we currently use and it
> would have its own DNS name?

Right, I imagine this will be at 'nfss.ubuntu.com' or similar.

--
Thomi Richards
<email address hidden>

647. By Thomi Richards on 2014-07-01

Add link to the restish precise link in the new trusty folder.

PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:647
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/989/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/989/rebuild

review: Approve (continuous-integration)
Francis Ginther (fginther) wrote :

I've been able to deploy this with the symlink fix. Also my other immediate concerns have been answered and I think there is a sufficient plan in place to address the other comments I had.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charms/precise/restish/hooks/hooks.py'
2--- charms/precise/restish/hooks/hooks.py 2014-06-25 16:50:44 +0000
3+++ charms/precise/restish/hooks/hooks.py 2014-07-01 22:22:26 +0000
4@@ -79,7 +79,7 @@
5 if framework == 'restish':
6 pkgs.append('python-restish')
7 elif framework == 'pyramid':
8- pkgs.append('python-pyramid')
9+ pkgs.append('python3-pyramid')
10 else:
11 juju_info('Invalid framework specified: {}'.format(framework))
12 sys.exit(1)
13
14=== added directory 'charms/trusty'
15=== added symlink 'charms/trusty/restish'
16=== target is u'../precise/restish/'
17=== added file 'juju-deployer/configs/nfss_http_vhost'
18--- juju-deployer/configs/nfss_http_vhost 1970-01-01 00:00:00 +0000
19+++ juju-deployer/configs/nfss_http_vhost 2014-07-01 22:22:26 +0000
20@@ -0,0 +1,19 @@
21+<VirtualHost *:80>
22+ ServerAdmin ci-engineering-private@lists.launchpad.net
23+ ErrorLog ${APACHE_LOG_DIR}/nfss-error.log
24+ LogLevel warn
25+ CustomLog ${APACHE_LOG_DIR}/nfss-access.log combined
26+
27+ DocumentRoot /srv/ci_airline_nfss/nf-stats-service/web_static
28+ <Directory />
29+ Options FollowSymLinks
30+ AllowOverride None
31+ Order allow,deny
32+ allow from all
33+ Require all granted
34+ </Directory>
35+
36+ ProxyPass /api http://{{ciairlinenfssrestish}}/api
37+ ProxyPassReverse /api http://{{ciairlinenfssrestish}}/api
38+ ProxyPreserveHost On
39+</VirtualHost>
40
41=== modified file 'juju-deployer/deploy.py'
42--- juju-deployer/deploy.py 2014-07-01 15:54:33 +0000
43+++ juju-deployer/deploy.py 2014-07-01 22:22:26 +0000
44@@ -715,14 +715,14 @@
45 fingerprint = build_payload(ball.name)
46 # The sha-1 fingerprint is used as the name of the file in swift so we
47 # can avoid uploading if it's already there.
48- if fingerprint not in ds.list_files():
49+ if fingerprint + '.tgz' not in ds.list_files():
50 with open(ball.name) as f:
51 print('Uploading local branch,'
52 ' fingerprint {}'.format(fingerprint))
53- payload_url = ds.put_file(fingerprint, f)
54+ payload_url = ds.put_file(fingerprint + '.tgz', f)
55 else:
56 print('Reusing local branch, fingerprint {}'.format(fingerprint))
57- payload_url = ds.file_path(fingerprint)
58+ payload_url = ds.file_path(fingerprint + '.tgz')
59 # the django charm uses branch for both meanings of payload
60 os.environ['CI_CODE_SOURCE'] = 'tarball'
61 os.environ['CI_PAYLOAD_URL'] = payload_url
62
63=== added file 'juju-deployer/nf-stats-service.yaml.tmpl'
64--- juju-deployer/nf-stats-service.yaml.tmpl 1970-01-01 00:00:00 +0000
65+++ juju-deployer/nf-stats-service.yaml.tmpl 2014-07-01 22:22:26 +0000
66@@ -0,0 +1,51 @@
67+ci-airline-experimental:
68+ series: trusty
69+ services:
70+ ci-airline-nfss-restish:
71+ expose: True
72+ charm: restish
73+ options:
74+ vcs: ${CI_CODE_SOURCE}
75+ branch: ${CI_BRANCH}
76+ tarball: ${CI_PAYLOAD_URL}
77+ framework: pyramid
78+ db_migration_cmd: "cd nf-stats-service && python3 -m nfss database-migrate"
79+ cron_cmd: "nf-stats-service/clean_db.sh"
80+ packages: "python-jinja2 python3-psycopg2 python3-oauthlib"
81+ python_path: ./nf-stats-service:./ci-utils
82+ json_status_path: api/v1/status
83+ install_sources: |
84+ - ${CI_PPA}
85+ install_keys: |
86+ - ""
87+ nagios_check_http_params: -H nfss -I 127.0.0.1 -e '
88+ 200 OK' --url='/api/v1/' -p 8080
89+ nagios_context: ci-airline-staging
90+ ci-airline-nfss-gunicorn:
91+ charm: gunicorn
92+ branch: lp:~canonical-ci-engineering/charms/trusty/gunicorn/gunicorn-py3-support@34
93+ options:
94+ wsgi_wsgi_file: nfss:app
95+ use_python3: true
96+ ci-airline-nfss-apache:
97+ charm: apache2
98+ branch: lp:charms/trusty/apache2@54
99+ expose: true
100+ options:
101+ enable_modules: "proxy proxy_http"
102+ vhost_http_template: include-base64://configs/nfss_http_vhost
103+ servername: ci_airline_nfss_apache
104+ ci-airline-nfss-content-fetcher:
105+ charm: content-fetcher
106+ branch: lp:~gnuoy/charms/precise/content-fetcher/trunk@53
107+ options:
108+ archive_location: ${CI_PAYLOAD_URL}
109+ dest_dir: /srv/ci_airline_nfss
110+ ci-airline-nfss-postgres:
111+ branch: lp:charms/trusty/postgresql@95
112+ charm: postgresql
113+ relations:
114+ - ["ci-airline-nfss-restish:wsgi", "ci-airline-nfss-gunicorn:wsgi-file"]
115+ - ["ci-airline-nfss-restish:pgsql", "ci-airline-nfss-postgres:db"]
116+ - ["ci-airline-nfss-apache:reverseproxy", "ci-airline-nfss-restish:website"]
117+ - ["ci-airline-nfss-apache", "ci-airline-nfss-content-fetcher"]
118
119=== added directory 'nf-stats-service'
120=== added file 'nf-stats-service/README'
121--- nf-stats-service/README 1970-01-01 00:00:00 +0000
122+++ nf-stats-service/README 2014-07-01 22:22:26 +0000
123@@ -0,0 +1,37 @@
124+ReadMe
125+======
126+
127+Documentation for the Non Functional Stats Service (NFSS). NFSS is composed of a few different parts:
128+
129+* A python3 / pyramid app that exposes a RESTful interface to a postgres data store.
130+* A client-side javascript-based UI that talks to the above.
131+
132+While the system is generally very simple, there are a few things that sysadmins ought to be aware of:
133+
134+Generating Client Keys
135+-----------------------
136+
137+Test data is submitted to the data store via the RESTful api, and this API call is secured with oauth. Client access keys need to be generated for every external client that wants to be able to post data into the database. This is achieved by changing to the nf-stats-service directory and running:
138+
139+$ python3 -m nfss keys-add
140+
141+This is an interactive script that will ask for client details, and finally will write a python script that can be used by the external client to insert data into the data store.
142+
143+A list of client keys can be generated in a similar fashion:
144+
145+$ python3 -m nfss keys-list
146+
147+A specific client id can be revoked by specifying it's client access key like so:
148+
149+$ python3 -m nfss keys-del cj2DriLAGxmxinDyJzvDVQVltRSLNI
150+
151+(obviously the client key will change, this is just an example).
152+
153+Cleaning the database
154+----------------------
155+
156+Part of the oauth authentication scheme involves storing nonce values in the database. In order to prevent this table from filling up, we install a daily cron job that cleans the database. This can be achieved manually by running:
157+
158+$ python3 -m nfss database-clean
159+
160+Although this should never need to be done manually, since the restish charm installs a cron daily job to run this command.
161\ No newline at end of file
162
163=== added file 'nf-stats-service/clean_db.sh'
164--- nf-stats-service/clean_db.sh 1970-01-01 00:00:00 +0000
165+++ nf-stats-service/clean_db.sh 2014-07-01 22:22:26 +0000
166@@ -0,0 +1,6 @@
167+#!/bin/sh -e
168+
169+# Clean the database. This is linked by the restish charm to /etc/cron.daily
170+
171+export PYTHONPATH=`dirname $0`
172+python3 -m nfss database-clean
173
174=== added directory 'nf-stats-service/db_patches'
175=== added file 'nf-stats-service/db_patches/000-initial_schema.sql'
176--- nf-stats-service/db_patches/000-initial_schema.sql 1970-01-01 00:00:00 +0000
177+++ nf-stats-service/db_patches/000-initial_schema.sql 2014-07-01 22:22:26 +0000
178@@ -0,0 +1,94 @@
179+-- Non Functional Test Metrics Database Schema.
180+
181+CREATE SEQUENCE project_id_seq;
182+CREATE TABLE project (
183+ id INTEGER PRIMARY KEY default nextval('project_id_seq'),
184+ name varchar(128) NOT NULL,
185+ UNIQUE (name)
186+);
187+ALTER SEQUENCE project_id_seq owned by project.id;
188+CREATE INDEX project_name_index ON project(name);
189+
190+CREATE SEQUENCE test_id_seq;
191+CREATE TABLE test (
192+ id INTEGER PRIMARY KEY default nextval('test_id_seq'),
193+ name varchar(128) NOT NULL,
194+ project_id integer,
195+ FOREIGN KEY (project_id) REFERENCES project(id),
196+ UNIQUE (project_id, name)
197+);
198+ALTER SEQUENCE test_id_seq owned by test.id;
199+CREATE INDEX test_name_index ON test(name);
200+
201+-- The client table stores secret resource owner keys, as well as public
202+-- client and resource owner keys. We also store some information about
203+-- the client.
204+-- The 'valid' column defaults to true. We only accept connections from
205+-- clients where this is set to 'true' and we set it to 'false' when
206+-- invalidating client keys. We do this rather than deleting the key
207+-- because we still want to retain historical information about which
208+-- data points were inserted by which client.
209+CREATE SEQUENCE client_id_seq;
210+CREATE TABLE client (
211+ id INTEGER PRIMARY KEY default nextval('client_id_seq'),
212+ access_key VARCHAR(30) NOT NULL,
213+ name TEXT,
214+ description TEXT,
215+ point_of_contact TEXT,
216+ resource_owner_key VARCHAR(30) NOT NULL,
217+ resource_owner_secret VARCHAR(30) NOT NULL,
218+ valid BOOLEAN default true,
219+ UNIQUE (access_key)
220+);
221+ALTER SEQUENCE client_id_seq owned by client.id;
222+-- we'll often want to look up a specific client access key, so let's index that:
223+CREATE INDEX client_access_key_index ON client(access_key);
224+-- We need to have a dummy client row added so the auth process is constant time:
225+-- Note that the resource owner key and secret need to be valid values, but can
226+-- never be authenticated against.
227+INSERT INTO client (access_key, name, resource_owner_key, resource_owner_secret, valid) VALUES (
228+ 'dummy-client-access-token',
229+ 'Dummy client',
230+ 'O9MFI1GUCrzBU6Uv1vshLX5wvhjDji',
231+ '{wtgn,V>$#bn1fwqbe&,}/l1STF<@Y',
232+ false
233+);
234+-- We also add a client that's invalid (can never be authenticated) that's used
235+-- for all the pre-populated data in the system.
236+INSERT INTO client (access_key, name, description, resource_owner_key, resource_owner_secret, valid) VALUES (
237+ 'sys-prepopulated-data-client',
238+ 'System Client',
239+ 'This is not a real client - it is used to link to pre-populated data.',
240+ 'Bad resource owner key',
241+ 'Bad resource owner secret',
242+ false
243+);
244+
245+CREATE SEQUENCE data_id_seq;
246+CREATE TABLE data (
247+ id INTEGER PRIMARY KEY default nextval('data_id_seq'),
248+ date_entered timestamp with time zone NOT NULL,
249+ data json NOT NULL,
250+ test_id integer,
251+ client_id integer,
252+ FOREIGN KEY (test_id) REFERENCES test(id),
253+ FOREIGN KEY (client_id) REFERENCES client(id)
254+);
255+ALTER SEQUENCE data_id_seq owned by data.id;
256+-- We will frequently be looking for data between two date ranges. Index the
257+-- date_entered column so this is efficient:
258+CREATE INDEX data_date_entered_index ON data(date_entered);
259+
260+
261+-- We need to store nonce values in the database. The nonce and timestamp are
262+-- stored with the access key and the resource owner key.
263+CREATE TABLE nonce (
264+ access_key VARCHAR(30) NOT NULL,
265+ timestamp timestamp without time zone NOT NULL,
266+ nonce TEXT NOT NULL,
267+ resource_owner_key TEXT
268+);
269+-- we'll always be searching for entries in all columns, so index that:
270+CREATE UNIQUE INDEX nonce_all_index ON nonce(access_key, timestamp, nonce, resource_owner_key);
271+-- We'll also be cleaning this table out periodically by sorting on the timestamp column:
272+CREATE INDEX nonce_timestamp_index ON nonce(timestamp);
273
274=== added directory 'nf-stats-service/nfss'
275=== added file 'nf-stats-service/nfss/__init__.py'
276--- nf-stats-service/nfss/__init__.py 1970-01-01 00:00:00 +0000
277+++ nf-stats-service/nfss/__init__.py 2014-07-01 22:22:26 +0000
278@@ -0,0 +1,55 @@
279+#!/usr/bin/python3
280+# Ubuntu CI Engine
281+# Copyright 2014 Canonical Ltd.
282+
283+# This program is free software: you can redistribute it and/or modify it
284+# under the terms of the GNU Affero General Public License version 3, as
285+# published by the Free Software Foundation.
286+
287+# This program is distributed in the hope that it will be useful, but
288+# WITHOUT ANY WARRANTY; without even the implied warranties of
289+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
290+# PURPOSE. See the GNU Affero General Public License for more details.
291+
292+# You should have received a copy of the GNU Affero General Public License
293+# along with this program. If not, see <http://www.gnu.org/licenses/>.
294+# import os
295+
296+from wsgiref.simple_server import make_server
297+from pyramid.config import Configurator
298+
299+
300+from nfss.api import v1 as api_v1
301+from nfss.database import get_connection_for_request
302+
303+
304+def make_wsgi_app():
305+ config = Configurator()
306+
307+ # Instead of configuring all the routes here, we defer to the individual
308+ # API version. This allows us to easily add 'v2' in the future. The actual
309+ # routes are added in the 'configure_routes' callable.
310+ #
311+ # The root app is served at '/api/v1/' - note the trailing '/'.
312+ config.include(api_v1.configure_routes, route_prefix='/api/v1')
313+ config.scan()
314+ try:
315+ import pyramid_debugtoolbar
316+ config.include('pyramid_debugtoolbar')
317+ config.registry.settings['debugtoolbar.hosts'].append('10.0.0.0/16')
318+ except ImportError:
319+ pass
320+
321+ config.add_request_method(
322+ get_connection_for_request,
323+ 'database',
324+ )
325+ return config.make_wsgi_app()
326+
327+
328+app = make_wsgi_app()
329+
330+
331+if __name__ == '__main__':
332+ server = make_server('127.0.0.1', 8000, app)
333+ server.serve_forever()
334
335=== added file 'nf-stats-service/nfss/__main__.py'
336--- nf-stats-service/nfss/__main__.py 1970-01-01 00:00:00 +0000
337+++ nf-stats-service/nfss/__main__.py 2014-07-01 22:22:26 +0000
338@@ -0,0 +1,220 @@
339+#!/usr/bin/env python3
340+# Ubuntu CI Engine
341+# Copyright 2014 Canonical Ltd.
342+#
343+# This program is free software: you can redistribute it and/or modify it
344+# under the terms of the GNU Affero General Public License version 3, as
345+# published by the Free Software Foundation.
346+#
347+# This program is distributed in the hope that it will be useful, but
348+# WITHOUT ANY WARRANTY; without even the implied warranties of
349+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
350+# PURPOSE. See the GNU Affero General Public License for more details.
351+#
352+# You should have received a copy of the GNU Affero General Public License
353+# along with this program. If not, see <http://www.gnu.org/licenses/>.
354+
355+from oauthlib.common import (
356+ generate_client_id,
357+ generate_token,
358+)
359+from argparse import ArgumentParser
360+import string
361+from textwrap import dedent
362+import sys
363+
364+import nfss
365+
366+
367+def main():
368+ parser = ArgumentParser('nfss')
369+ subparsers = parser.add_subparsers(help='Commands', dest="command")
370+
371+ subparsers.add_parser(
372+ 'keys-add',
373+ help="Generate a new set of access keys.",
374+ )
375+ parser_keys_del = subparsers.add_parser(
376+ 'keys-del',
377+ help="Remove an existing set of access keys.",
378+ )
379+ parser_keys_del.add_argument(
380+ 'client_key',
381+ help="The client access key you want to invalidate."
382+ )
383+ subparsers.add_parser(
384+ 'keys-list',
385+ help="List current client access keys."
386+ )
387+ subparsers.add_parser(
388+ 'database-migrate',
389+ help="Migrate the database.",
390+ )
391+ subparsers.add_parser(
392+ 'database-clean',
393+ help="Run database maintenance tasks."
394+ )
395+ arguments = parser.parse_args()
396+ if arguments.command is None:
397+ parser.error("Missing command string.")
398+ return
399+
400+ if arguments.command == 'keys-add':
401+ keys_add()
402+ elif arguments.command == 'keys-del':
403+ keys_del(arguments.client_key)
404+ elif arguments.command == 'keys-list':
405+ keys_list()
406+ elif arguments.command == 'database-migrate':
407+ nfss.db_migrate.main()
408+ elif arguments.command == 'database-clean':
409+ database_clean()
410+
411+
412+def keys_add():
413+ global INSERT_SCRIPT_TEMPLATE
414+ repeat = True
415+ name = description = poc = ""
416+ while repeat:
417+ name = input("Client name: ")
418+ poc = input("Point of contact (name & email address): ")
419+ description = input("Description of client: ")
420+ print(
421+ dedent(
422+ """
423+ Client Summary:
424+
425+ Client Name: {}
426+ Point of Contact: {}
427+ Description: {}
428+
429+ """.format(name, poc, description)
430+ )
431+ )
432+ correct = input("Is this information correct (y/n)? ")
433+ repeat = correct.lower().strip() == "n"
434+
435+ access_key = generate_token()
436+ owner_key = generate_token()
437+ owner_secret = generate_client_id()
438+
439+ with nfss.database.get_scoped_connection() as database_connection:
440+ nfss.database.auth_add_client_details(
441+ database_connection,
442+ name,
443+ description,
444+ poc,
445+ access_key,
446+ owner_key,
447+ owner_secret
448+ )
449+
450+ valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
451+ filename = ''.join(c for c in name if c in valid_chars)
452+ filename = filename.replace(' ', '_') + '_insert.py'
453+ while True:
454+ try:
455+ with open(filename, 'w') as f:
456+ f.write(
457+ INSERT_SCRIPT_TEMPLATE.format(
458+ client_access_key=access_key,
459+ resource_owner_key=owner_key,
460+ resource_owner_secret=owner_secret,
461+ )
462+ )
463+ except IOError:
464+ print("Cannot write to the default location (%s)" % filename)
465+ filename = input("Where should the new script be written to? ")
466+ else:
467+ print(
468+ "%r has been written. Use this to insert data into the data store." %
469+ filename
470+ )
471+ break
472+
473+
474+def keys_del(client_key):
475+ with nfss.database.get_scoped_connection() as db_connection:
476+ if not nfss.database.get_auth_client_key_exists(
477+ db_connection,
478+ client_key
479+ ):
480+ print("Error: the client key '%s' does not exist. Use 'keys-list'")
481+ print(" to get a list of all the client access keys.")
482+ sys.exit(1)
483+ nfss.database.invalidate_client_key(db_connection, client_key)
484+ if not nfss.database.get_auth_client_key_exists(
485+ db_connection,
486+ client_key
487+ ):
488+ print("Client key has been successfully invalidated.")
489+ else:
490+ print("Client key has NOT been invalidated.")
491+
492+
493+def keys_list():
494+ with nfss.database.get_scoped_connection() as db_connection:
495+ data = nfss.database.get_auth_client_key_list(db_connection)
496+ if data:
497+ keylen = max([len(r[0]) for r in data]) + 2
498+ namelen = max([len(r[1]) for r in data]) + 2
499+ desclen = max([len(r[2]) for r in data]) + 2
500+ format_str = "%%%ds %%%ds %%%ds %%s" % (keylen, namelen, desclen)
501+ print(format_str % ("Key:", "Name:", "Description:", "Contact:"))
502+ for key, name, desc, poc in data:
503+ print(format_str % (key, name, desc, poc))
504+ print("Total: %d keys" % len(data))
505+
506+
507+def database_clean():
508+ with nfss.database.get_scoped_connection() as db_connection:
509+ nfss.database.clean_old_nonces(db_connection)
510+
511+
512+INSERT_SCRIPT_TEMPLATE = r'''#!/usr/bin/env python3
513+
514+import json
515+import sys
516+from requests_oauthlib import OAuth1Session
517+
518+# Note: The resource_owner_secret is *secret*, and must be kept secret. If
519+# you think it's been compromised, contact IS with your client access key
520+# and they can invalidate the old key and assign a new one.
521+client_access_key = {client_access_key!r}
522+resource_owner_key = {resource_owner_key!r}
523+resource_owner_secret = {resource_owner_secret!r}
524+backend = 'http://nfss.ubuntu.com/api/v1'
525+
526+if len(sys.argv) != 3:
527+ print("Usage: %s <projectname> <testname>\n" % sys.argv[0])
528+ print("Pipe json test data to this script to insert it into the database.")
529+ sys.exit(1)
530+
531+project = sys.argv[1]
532+test = sys.argv[2]
533+
534+if sys.stdin.isatty():
535+ print("Error: Pipe json test data through this script, like so:")
536+ print("$ cat test_data | %s %s %s" % (sys.argv[0], project, test))
537+ sys.exit(2)
538+
539+data = sys.stdin.read()
540+try:
541+ json.loads(data)
542+except ValueError as e:
543+ print("Error: Data does not appear to be valid json: %s" % e)
544+ sys.exit(3)
545+
546+test_session = OAuth1Session(
547+ client_access_key,
548+ resource_owner_key=resource_owner_key,
549+ resource_owner_secret=resource_owner_secret
550+)
551+url = '/'.join((backend, project, test))
552+r = test_session.post(url, dict(data=data.encode()))
553+print(r.text)
554+'''
555+
556+
557+if __name__ == '__main__':
558+ main()
559
560=== added directory 'nf-stats-service/nfss/api'
561=== added file 'nf-stats-service/nfss/api/__init__.py'
562--- nf-stats-service/nfss/api/__init__.py 1970-01-01 00:00:00 +0000
563+++ nf-stats-service/nfss/api/__init__.py 2014-07-01 22:22:26 +0000
564@@ -0,0 +1,14 @@
565+# Ubuntu CI Engine
566+# Copyright 2014 Canonical Ltd.
567+
568+# This program is free software: you can redistribute it and/or modify it
569+# under the terms of the GNU Affero General Public License version 3, as
570+# published by the Free Software Foundation.
571+
572+# This program is distributed in the hope that it will be useful, but
573+# WITHOUT ANY WARRANTY; without even the implied warranties of
574+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
575+# PURPOSE. See the GNU Affero General Public License for more details.
576+
577+# You should have received a copy of the GNU Affero General Public License
578+# along with this program. If not, see <http://www.gnu.org/licenses/>.
579
580=== added file 'nf-stats-service/nfss/api/v1.py'
581--- nf-stats-service/nfss/api/v1.py 1970-01-01 00:00:00 +0000
582+++ nf-stats-service/nfss/api/v1.py 2014-07-01 22:22:26 +0000
583@@ -0,0 +1,140 @@
584+# Ubuntu CI Engine
585+# Copyright 2014 Canonical Ltd.
586+
587+# This program is free software: you can redistribute it and/or modify it
588+# under the terms of the GNU Affero General Public License version 3, as
589+# published by the Free Software Foundation.
590+
591+# This program is distributed in the hope that it will be useful, but
592+# WITHOUT ANY WARRANTY; without even the implied warranties of
593+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
594+# PURPOSE. See the GNU Affero General Public License for more details.
595+
596+# You should have received a copy of the GNU Affero General Public License
597+# along with this program. If not, see <http://www.gnu.org/licenses/>.
598+
599+"""V1 of the REST API.
600+
601+This file contains the views for the v1 API, as well as the URL routing table
602+for the v1 API.
603+
604+"""
605+from datetime import (
606+ datetime,
607+ timedelta,
608+ timezone,
609+)
610+import json
611+from pyramid.view import view_config
612+from pyramid.httpexceptions import HTTPBadRequest
613+
614+from nfss import database
615+from nfss.auth import oauth_protected
616+
617+
618+UTC = timezone(timedelta(0))
619+
620+
621+def configure_routes(config):
622+ """Add url routes to 'config' object."""
623+ config.add_route('v1.root', '/')
624+ config.add_route('v1.project', '/{project}')
625+ config.add_route('v1.test', '/{project}/{test}')
626+
627+
628+@view_config(route_name='v1.root', renderer='json', request_method='GET')
629+def root(request):
630+ """Return summary data about the projects and tests."""
631+ data = database.get_details_for_all_projects(request.database())
632+ return dict(
633+ projects={
634+ r[0]: {
635+ 'tests': json.loads(r[1]),
636+ 'last_updated': r[2].isoformat(),
637+ 'path': request.route_path('v1.project', project=r[0])
638+ } for r in data},
639+ )
640+
641+
642+@view_config(route_name='v1.project', renderer='json', request_method='GET')
643+def project(request):
644+ """Return detailed information about this project's tests."""
645+ project_name = request.matchdict['project']
646+ project_data = database.get_details_for_project(
647+ request.database(),
648+ project_name
649+ )
650+
651+ for test in project_data:
652+ project_data[test]['path'] = request.route_path(
653+ 'v1.test',
654+ project=project_name,
655+ test=test
656+ )
657+ return dict(project_name=project_name, tests=project_data)
658+
659+
660+@view_config(route_name='v1.test', renderer='json', request_method='GET')
661+def test(request):
662+ """Return data points for this particular test.
663+
664+ Check the Range HTTP header, or return the last 30 days worth of data if
665+ it's not present.
666+
667+ """
668+ # import pudb; pudb.set_trace()
669+ project_name = request.matchdict['project']
670+ test_name = request.matchdict['test']
671+ start = try_parse_date(request.params.get('start_date', None))
672+ end = try_parse_date(request.params.get('end_date', None))
673+ data, f, l, df, dl = database.get_details_for_test(
674+ request.database(),
675+ project_name,
676+ test_name,
677+ start,
678+ end,
679+ )
680+ return dict(
681+ project_name=project_name,
682+ test_name=test_name,
683+ first_data=f.isoformat(),
684+ last_data=l.isoformat(),
685+ data_range_first=df.isoformat(),
686+ data_range_last=dl.isoformat(),
687+ data=json.loads(data) if data else [],
688+ )
689+
690+
691+def try_parse_date(date_param):
692+ if date_param is not None:
693+ try:
694+ return datetime.fromtimestamp(float(date_param), UTC)
695+ except:
696+ return None
697+ return None
698+
699+
700+@view_config(route_name='v1.test', renderer='json', request_method='POST')
701+@oauth_protected()
702+def test_add(request, oauth_request):
703+ """Add data points to this test.
704+
705+ Return the created id of the data point added to the DB if it was
706+ successful, or an error if it wasn't.
707+
708+ """
709+ try:
710+ project_name = request.matchdict['project']
711+ test_name = request.matchdict['test']
712+ data = request.params['data']
713+ except KeyError as e:
714+ raise HTTPBadRequest("Missing data in POST request: %s" % e)
715+ else:
716+ created_id = database.insert_test_data(
717+ request.database(),
718+ project_name,
719+ test_name,
720+ data,
721+ oauth_request.client_key
722+ )
723+ return dict(created_id=created_id)
724
725=== added file 'nf-stats-service/nfss/auth.py'
726--- nf-stats-service/nfss/auth.py 1970-01-01 00:00:00 +0000
727+++ nf-stats-service/nfss/auth.py 2014-07-01 22:22:26 +0000
728@@ -0,0 +1,108 @@
729+#!/usr/bin/env python3
730+# Ubuntu CI Engine
731+# Copyright 2014 Canonical Ltd.
732+#
733+# This program is free software: you can redistribute it and/or modify it
734+# under the terms of the GNU Affero General Public License version 3, as
735+# published by the Free Software Foundation.
736+#
737+# This program is distributed in the hope that it will be useful, but
738+# WITHOUT ANY WARRANTY; without even the implied warranties of
739+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
740+# PURPOSE. See the GNU Affero General Public License for more details.
741+#
742+# You should have received a copy of the GNU Affero General Public License
743+# along with this program. If not, see <http://www.gnu.org/licenses/>.
744+
745+from oauthlib.oauth1 import RequestValidator, WebApplicationServer
746+from pyramid.httpexceptions import HTTPForbidden
747+import functools
748+
749+from nfss import database
750+
751+
752+class NFSSRequestValidator(RequestValidator):
753+
754+ def __init__(self, database_connection):
755+ super().__init__()
756+ self._database = database_connection
757+
758+ @property
759+ def enforce_ssl(self):
760+ return False
761+
762+ @property
763+ def dummy_client(self):
764+ return "dummy-client-access-token"
765+
766+ def validate_client_key(self, client_key, request):
767+ return database.get_auth_client_key_exists(self._database, client_key)
768+
769+ def validate_access_token(self, client_key, resource_owner_key, request):
770+ return database.get_auth_resource_owner_key_for_client_key(
771+ self._database,
772+ client_key
773+ )
774+
775+ def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
776+ request, request_token=None,
777+ access_token=None):
778+ token = request_token or access_token
779+ if not database.get_auth_nonce_already_used(
780+ self._database,
781+ client_key,
782+ timestamp,
783+ nonce,
784+ token
785+ ):
786+ database.store_nonce(
787+ self._database,
788+ client_key,
789+ timestamp,
790+ nonce,
791+ token
792+ )
793+ return True
794+ return False
795+
796+ def validate_realms(self, client_key, token, request, uri=None,
797+ realms=None):
798+ # Realms are used so we can split the app into several sections with
799+ # different authentications in each. We're not using realms, so we
800+ # just return True straight away:
801+ return True
802+
803+ def get_client_secret(self, client_key, request):
804+ # We don't use the client secret, so this will always return the
805+ # empty string.
806+ return ''
807+
808+ def get_access_token_secret(self, client_key, owner_key, request):
809+ return database.get_auth_resource_owner_secret_for_client_key(
810+ self._database,
811+ client_key
812+ )
813+
814+
815+def oauth_protected(realms=None):
816+ def wrapper(f):
817+ @functools.wraps(f)
818+ def verify_oauth(request, *args, **kwargs):
819+ provider = WebApplicationServer(
820+ NFSSRequestValidator(
821+ request.database()
822+ )
823+ )
824+ v, r = provider.validate_protected_resource_request(
825+ request.url,
826+ http_method=request.method,
827+ body=request.body,
828+ headers=request.headers,
829+ realms=realms or []
830+ )
831+ if v:
832+ return f(request, r, *args, **kwargs)
833+ else:
834+ raise HTTPForbidden()
835+ return verify_oauth
836+ return wrapper
837
838=== added file 'nf-stats-service/nfss/database.py'
839--- nf-stats-service/nfss/database.py 1970-01-01 00:00:00 +0000
840+++ nf-stats-service/nfss/database.py 2014-07-01 22:22:26 +0000
841@@ -0,0 +1,454 @@
842+# Ubuntu CI Engine
843+# Copyright 2014 Canonical Ltd.
844+
845+# This program is free software: you can redistribute it and/or modify it
846+# under the terms of the GNU Affero General Public License version 3, as
847+# published by the Free Software Foundation.
848+
849+# This program is distributed in the hope that it will be useful, but
850+# WITHOUT ANY WARRANTY; without even the implied warranties of
851+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
852+# PURPOSE. See the GNU Affero General Public License for more details.
853+
854+# You should have received a copy of the GNU Affero General Public License
855+# along with this program. If not, see <http://www.gnu.org/licenses/>.
856+
857+"""Functions that access the database."""
858+
859+import contextlib
860+import os.path
861+import functools
862+import json
863+import logging
864+
865+logger = logging.getLogger(__name__)
866+
867+import psycopg2.pool
868+
869+
870+def get_settings_path():
871+ return os.path.abspath(
872+ os.path.join(
873+ os.path.dirname(__file__),
874+ '..',
875+ '..',
876+ 'pgsql.json'
877+ )
878+ )
879+
880+
881+@functools.lru_cache()
882+def get_db_settings(settings_path):
883+ """Return a dictionary containing the database settings, as provided by
884+ juju.
885+
886+ If the settings file does not exist an IOError is returned.
887+ If the settings file exists, but is not valid JSON, a ValueError is
888+ returned.
889+
890+ """
891+
892+ with open(settings_path, 'r') as settings_file:
893+ return json.load(settings_file)
894+
895+
896+def translate_juju_db_settings(db_settings):
897+ """Given a dictionary of juju database settings, return a dictionary of
898+ psycopg2 database connection settings.
899+
900+ """
901+ return dict(
902+ database=db_settings['database'],
903+ user=db_settings['schema_user'],
904+ password=db_settings['schema_password'],
905+ host=db_settings['host'],
906+ port=db_settings['port'],
907+ )
908+
909+
910+def get_connection_for_request(request=None):
911+ """Get a database connection.
912+
913+ This is bound to every request object we recieve as 'request.database'.
914+
915+ Note: If this argument is not called with the 'request' parameter then the
916+ caller *must* call '.close()' on the returned connection object.
917+
918+ """
919+ juju_db_settings = get_db_settings(get_settings_path())
920+ db_kwargs = translate_juju_db_settings(juju_db_settings)
921+ connection = psycopg2.connect(**db_kwargs)
922+ if request is not None:
923+ def cleanup(request):
924+ connection.close()
925+ request.add_finished_callback(cleanup)
926+ return connection
927+
928+
929+@contextlib.contextmanager
930+def get_scoped_connection():
931+ """Get a database connection.
932+
933+ This context manager will return a database connection, and will
934+ automatically close the connection when the context manager exits.
935+
936+ Use like so::
937+
938+ with nfss.database.get_scoped_connection() as db:
939+ cursor = db.cursor()
940+ cursor.execute('SELECT 1+1;')
941+ db.commit()
942+
943+ """
944+ connection = get_connection_for_request()
945+ try:
946+ yield connection
947+ finally:
948+ connection.close()
949+
950+
951+def autocommit(fn):
952+ """A simple utility decorator that wraps a method that gets content from
953+ the database. Using this will ensure that:
954+
955+ 1) If the wrapped function returns without raising an exception, the
956+ connection's 'commit()' method is called.
957+ 2) If the wrapped function raises an exception, the connection's
958+ 'rollback()' method will be called.
959+
960+ This is important since by default, any operation (including simple
961+ SELECT queries) starts a new transaction. Without calling 'commit' or
962+ 'rollback' these transactions gradually use up more and more resources.
963+
964+ """
965+ def worker(connection, *args, **kwargs):
966+ try:
967+ result = fn(connection, *args, **kwargs)
968+ connection.commit()
969+ return result
970+ except Exception:
971+ connection.rollback()
972+ raise
973+ return worker
974+
975+
976+@autocommit
977+def get_details_for_all_projects(connection):
978+ """Get high level details for all projects in the system.
979+
980+ The return value will be a list of lists. Each inner list will contain:
981+
982+ [project_name, test_array, max_date]
983+
984+ * project_name is obvious - it's the name of the project.
985+ * test_array is a json formatted array of test names that have had data
986+ entered for this project.
987+ * max_date is the date of the last data point for this project in any test.
988+
989+ """
990+ cursor = connection.cursor()
991+ cursor.execute('''
992+ SELECT
993+ project.name,
994+ json_agg(distinct test.name),
995+ max(data.date_entered)
996+ FROM project
997+ INNER JOIN test ON project.id = test.project_id
998+ INNER JOIN data ON test.id = data.test_id
999+ GROUP BY project.name;
1000+ ''')
1001+ return cursor.fetchall()
1002+
1003+
1004+@autocommit
1005+def get_details_for_project(connection, project_name):
1006+ """Get high-level details for a single project.
1007+
1008+ The return value will be a json string containing the test name, first and
1009+ last data point dates, and a count of data points, in an array, for each
1010+ test.
1011+
1012+ """
1013+ cursor = connection.cursor()
1014+ cursor.execute(
1015+ '''
1016+ SELECT
1017+ test.name as test_name,
1018+ min(data.date_entered) as first_data,
1019+ max(data.date_entered) as last_data,
1020+ count(data.id) as data_points
1021+ FROM test
1022+ INNER JOIN data on data.test_id = test.id
1023+ INNER JOIN project on test.project_id = project.id
1024+ WHERE project.name = %s
1025+ GROUP BY test.name;
1026+ ''',
1027+ (project_name,),
1028+ )
1029+ data = {}
1030+ for row in cursor:
1031+ data[row[0]] = dict(
1032+ first_data=row[1].isoformat(),
1033+ last_data=row[2].isoformat(),
1034+ data_points=row[3]
1035+ )
1036+ return data
1037+
1038+
1039+@autocommit
1040+def get_details_for_test(connection, project_name, test_name, start_time=None,
1041+ end_time=None):
1042+ """Get data points for a particular project / test combo.
1043+
1044+ If start_time is specified, end time must also be specified. If either are
1045+ specified, they must be datetime objects. If neither are specified, then
1046+ the data range will be 30 days from the last data point.
1047+
1048+ This functions returns a tuple that contins, in order:
1049+
1050+ * A json string containing the test data points.
1051+ * A datetime object representing the point at which the returned data
1052+ range starts.
1053+ * A datetime object representing the point at which the returned data
1054+ range ends.
1055+ * A datetime object representing the first data point in the test data.
1056+ * A datetime object represending the last data point in the test data.
1057+
1058+ """
1059+ cursor = connection.cursor()
1060+
1061+ if all((start_time, end_time)):
1062+ cursor.execute(
1063+ '''
1064+ SELECT
1065+ min(data.date_entered),
1066+ max(data.date_entered)
1067+ FROM data
1068+ INNER JOIN test on data.test_id = test.id
1069+ INNER JOIN project on test.project_id = project.id
1070+ WHERE project.name = %s
1071+ AND test.name = %s;
1072+ ''',
1073+ (project_name, test_name)
1074+ )
1075+ first, last = cursor.fetchone()
1076+ cursor.execute(
1077+ '''
1078+ SELECT json_agg(d)
1079+ FROM (
1080+ SELECT
1081+ data.date_entered AS date_entered,
1082+ data.id AS id,
1083+ data.data AS data
1084+ FROM data
1085+ INNER JOIN test on data.test_id = test.id
1086+ INNER JOIN project on test.project_id = project.id
1087+ WHERE project.name = %s
1088+ AND test.name = %s
1089+ AND data.date_entered >= %s
1090+ AND data.date_entered <= %s
1091+ ) as d;
1092+ ''',
1093+ (project_name, test_name, start_time, end_time)
1094+ )
1095+ return (
1096+ cursor.fetchone()[0],
1097+ first,
1098+ last,
1099+ start_time,
1100+ end_time,
1101+ )
1102+ else:
1103+ # user didn't specify dates.
1104+ cursor.execute(
1105+ '''
1106+ SELECT
1107+ min(data.date_entered),
1108+ max(data.date_entered) - interval '30 days',
1109+ max(data.date_entered)
1110+ FROM data
1111+ INNER JOIN test on data.test_id = test.id
1112+ INNER JOIN project on test.project_id = project.id
1113+ WHERE project.name = %s
1114+ AND test.name = %s;
1115+ ''',
1116+ (project_name, test_name)
1117+ )
1118+ first, data_first, last = cursor.fetchone()
1119+ cursor.execute(
1120+ '''
1121+ SELECT json_agg(d)
1122+ FROM (
1123+ SELECT
1124+ data.date_entered AS date_entered,
1125+ data.id AS id,
1126+ data.data AS data
1127+ FROM data
1128+ INNER JOIN test on data.test_id = test.id
1129+ INNER JOIN project on test.project_id = project.id
1130+ WHERE project.name = %s
1131+ AND test.name = %s
1132+ AND data.date_entered >= %s
1133+ AND data.date_entered <= %s
1134+ ) as d;
1135+ ''',
1136+ (project_name, test_name, data_first, last)
1137+ )
1138+ return (
1139+ cursor.fetchone()[0],
1140+ first,
1141+ last,
1142+ data_first,
1143+ last,
1144+ )
1145+
1146+
1147+@autocommit
1148+def insert_test_data(connection, project_name, test_name, data, client_key):
1149+ # TODO; also link client that inserted it...
1150+ project_id = _maybe_create_project(connection, project_name)
1151+ test_id = _maybe_create_test(connection, project_id, test_name)
1152+ cursor = connection.cursor()
1153+ cursor.execute(
1154+ "INSERT INTO data (date_entered, data, test_id, client_id) "
1155+ "VALUES ('now', %s, %s, (SELECT id FROM client where access_key = %s))"
1156+ " RETURNING id;",
1157+ (data, test_id, client_key)
1158+ )
1159+ return cursor.fetchone()[0]
1160+
1161+
1162+def _maybe_create_project(connection, project_name):
1163+ """If the project exists, return it's id. If it doesn't, create it and
1164+ return it's id.
1165+
1166+ """
1167+ cursor = connection.cursor()
1168+ cursor.execute('SELECT id FROM project WHERE name = %s;', (project_name,))
1169+ if cursor.rowcount > 0:
1170+ return cursor.fetchone()[0]
1171+ cursor.execute(
1172+ 'INSERT INTO project (name) VALUES (%s) RETURNING id;',
1173+ (project_name,)
1174+ )
1175+ return cursor.fetchone()[0]
1176+
1177+
1178+def _maybe_create_test(connection, project_id, test_name):
1179+ cursor = connection.cursor()
1180+ cursor.execute(
1181+ 'SELECT id FROM test WHERE name = %s AND project_id = %s;',
1182+ (test_name, project_id)
1183+ )
1184+ if cursor.rowcount > 0:
1185+ return cursor.fetchone()[0]
1186+ cursor.execute(
1187+ 'INSERT INTO test (name, project_id) VALUES (%s, %s) RETURNING id;',
1188+ (test_name, project_id)
1189+ )
1190+ return cursor.fetchone()[0]
1191+
1192+
1193+@autocommit
1194+def get_auth_client_key_exists(connection, client_key):
1195+ cursor = connection.cursor()
1196+ cursor.execute(
1197+ 'SELECT id FROM client WHERE access_key = %s AND valid = true;',
1198+ (client_key,)
1199+ )
1200+ return cursor.rowcount != 0
1201+
1202+
1203+@autocommit
1204+def get_auth_client_key_list(connection):
1205+ """Get a list of valid client keys."""
1206+ cursor = connection.cursor()
1207+ cursor.execute(
1208+ 'SELECT access_key, name, description, point_of_contact '
1209+ 'FROM client WHERE valid = true;'
1210+ )
1211+ return cursor.fetchall()
1212+
1213+
1214+@autocommit
1215+def invalidate_client_key(connection, client_key):
1216+ cursor = connection.cursor()
1217+ cursor.execute(
1218+ 'UPDATE client SET valid = false WHERE access_key = %s;',
1219+ (client_key,)
1220+ )
1221+
1222+
1223+@autocommit
1224+def get_auth_resource_owner_key_for_client_key(connection, client_key):
1225+ cursor = connection.cursor()
1226+ cursor.execute(
1227+ 'SELECT resource_owner_key FROM client WHERE access_key = %s;',
1228+ (client_key,)
1229+ )
1230+ return cursor.fetchone()[0]
1231+
1232+
1233+@autocommit
1234+def auth_add_client_details(connection, name, description, poc, access_key,
1235+ owner_key, owner_secret):
1236+ cursor = connection.cursor()
1237+ cursor.execute(
1238+ '''
1239+ INSERT INTO client (access_key, name, description, point_of_contact,
1240+ resource_owner_key, resource_owner_secret)
1241+ VALUES (%s, %s, %s, %s, %s, %s);
1242+ ''',
1243+ (access_key, name, description, poc, owner_key, owner_secret)
1244+ )
1245+
1246+
1247+@autocommit
1248+def get_auth_resource_owner_secret_for_client_key(connection, client_key):
1249+ cursor = connection.cursor()
1250+ cursor.execute(
1251+ 'SELECT resource_owner_secret FROM client where access_key = %s;',
1252+ (client_key,)
1253+ )
1254+ return cursor.fetchone()[0]
1255+
1256+
1257+@autocommit
1258+def get_auth_nonce_already_used(conn, client_key, timestamp, nonce, owner_key):
1259+ cursor = conn.cursor()
1260+ cursor.execute(
1261+ '''
1262+ SELECT * FROM nonce
1263+ WHERE access_key = %s
1264+ AND timestamp = to_timestamp(%s)
1265+ AND nonce = %s
1266+ AND resource_owner_key = %s;
1267+ ''',
1268+ (client_key, timestamp, nonce, owner_key)
1269+ )
1270+ return cursor.rowcount != 0
1271+
1272+
1273+@autocommit
1274+def store_nonce(connection, client_key, timestamp, nonce, owner_key):
1275+ cursor = connection.cursor()
1276+ cursor.execute(
1277+ '''
1278+ INSERT INTO nonce (access_key, timestamp, nonce, resource_owner_key)
1279+ VALUES (%s, to_timestamp(%s), %s, %s);
1280+ ''',
1281+ (client_key, timestamp, nonce, owner_key)
1282+ )
1283+ return
1284+
1285+
1286+@autocommit
1287+def clean_old_nonces(connection):
1288+ cursor = connection.cursor()
1289+ cursor.execute(
1290+ '''
1291+ DELETE FROM nonce
1292+ WHERE timestamp <
1293+ CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - CAST('1 day' AS interval);
1294+ '''
1295+ )
1296
1297=== added file 'nf-stats-service/nfss/db_migrate.py'
1298--- nf-stats-service/nfss/db_migrate.py 1970-01-01 00:00:00 +0000
1299+++ nf-stats-service/nfss/db_migrate.py 2014-07-01 22:22:26 +0000
1300@@ -0,0 +1,162 @@
1301+#!/usr/bin/env python3
1302+# Ubuntu CI Engine
1303+# Copyright 2014 Canonical Ltd.
1304+#
1305+# This program is free software: you can redistribute it and/or modify it
1306+# under the terms of the GNU Affero General Public License version 3, as
1307+# published by the Free Software Foundation.
1308+#
1309+# This program is distributed in the hope that it will be useful, but
1310+# WITHOUT ANY WARRANTY; without even the implied warranties of
1311+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1312+# PURPOSE. See the GNU Affero General Public License for more details.
1313+#
1314+# You should have received a copy of the GNU Affero General Public License
1315+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1316+
1317+"""Migration script for the Non Functional Stats Service.
1318+
1319+This script is called when the service is installed, and any time the service
1320+is upgraded. It follows a few simple steps to manage the database version:
1321+
1322+1) Connecxt to the database, and read the version from the db_version table. If
1323+ it doesn't exist, assume the version is -1.
1324+
1325+2) Scan the db_patches directory for files that end in '.sql'. The files must
1326+ be named 'NNN-{name}.sql'. The NNN is the numeric order of the patches,
1327+ with leading '0's.
1328+
1329+3) Apply patches in order. Each patch is applied in a DB transaction, and after
1330+ each patch we incriment the database revision number in the db_version
1331+ table. As a special case, if we just upgraded to version '0', we create
1332+ the table.
1333+
1334+"""
1335+
1336+import glob
1337+import os.path
1338+import psycopg2
1339+import re
1340+import sys
1341+
1342+from nfss.database import get_scoped_connection
1343+
1344+
1345+def main():
1346+ with get_scoped_connection() as connection:
1347+ current_version = get_current_database_version(connection)
1348+ maximum_version = get_maximum_version()
1349+ print("Current database version is: %d" % current_version)
1350+ if current_version != maximum_version:
1351+ print(
1352+ "Database schema can be updated to version: %d" %
1353+ maximum_version
1354+ )
1355+ for new_version in range(current_version + 1, maximum_version + 1):
1356+ upgrade_database_to_version(connection, new_version)
1357+
1358+
1359+def get_current_database_version(connection):
1360+ cursor = connection.cursor()
1361+ try:
1362+ cursor.execute('SELECT version FROM db_version;')
1363+ except psycopg2.ProgrammingError:
1364+ # table doesn't exist, we have a blank database, and need to start from
1365+ # scratch.
1366+ connection.rollback()
1367+ return -1
1368+
1369+ data = cursor.fetchone()
1370+ if data is None:
1371+ # table exists, but contains no data - not sure what this means
1372+ # as we should never get here. Let's assume version -1.
1373+ return -1
1374+ else:
1375+ return int(data[0])
1376+
1377+
1378+def report_error(message):
1379+ # TODO: replace with proper logging!
1380+ sys.stderr.write(message + '\n')
1381+
1382+
1383+def get_maximum_version():
1384+ """Return the maximum version we can patch the database to."""
1385+ patches = get_all_patch_paths()
1386+ return int(patches[-1][:3])
1387+
1388+
1389+def get_all_patch_paths():
1390+ patches_path = get_database_patch_dir()
1391+ return sorted(
1392+ filter(
1393+ lambda f: re.match(r'\d{3}-.*.sql', f),
1394+ os.listdir(patches_path)
1395+ )
1396+ )
1397+
1398+
1399+def get_patch_file_path_for_version(version_number):
1400+ """Given a database version number, get the path to a patch that upgrades
1401+ the schema to that version.
1402+
1403+ raise a RuntimeError if more than one file matches.
1404+ """
1405+ matches = glob.glob(
1406+ os.path.join(
1407+ get_database_patch_dir(),
1408+ "%03d-*.sql" % version_number
1409+ )
1410+ )
1411+ if len(matches) > 1:
1412+ raise RuntimeError(
1413+ "More than one file matched for version %d: %r" % (
1414+ version_number, matches)
1415+ )
1416+ if not matches:
1417+ raise RuntimeError(
1418+ "No patch file found for version %d" % version_number
1419+ )
1420+ return matches[0]
1421+
1422+
1423+def get_database_patch_dir():
1424+ return os.path.abspath(
1425+ os.path.join(
1426+ os.path.dirname(__file__),
1427+ '..',
1428+ 'db_patches'
1429+ )
1430+ )
1431+
1432+
1433+def upgrade_database_to_version(connection, new_version):
1434+ """Do the work to upgrade the database to version 'new_version'.
1435+
1436+ This function starts a transaction on 'connection', and will either
1437+ commit or rollback the transaction before the function ends.
1438+
1439+ On success the function returns None, on failure the function raises an
1440+ exception.
1441+
1442+ Note: Only call this function if the database is at 'new_version' - 1.
1443+
1444+ """
1445+ patch_file = get_patch_file_path_for_version(new_version)
1446+ print("Upgrading to version %s ..." % new_version)
1447+ cursor = connection.cursor()
1448+ try:
1449+ with open(patch_file, 'r') as patch:
1450+ cursor.execute(patch.read())
1451+ if new_version == 0:
1452+ cursor.execute('CREATE TABLE db_version (version integer);')
1453+ cursor.execute('INSERT INTO db_version (version) VALUES (0);')
1454+ else:
1455+ cursor.execute('UPDATE db_version SET version=%d'
1456+ % new_version)
1457+ except Exception as err:
1458+ print("Error: %s" % err)
1459+ connection.rollback()
1460+ raise
1461+ print("Done!")
1462+ connection.commit()
1463
1464=== added directory 'nf-stats-service/nfss/tests'
1465=== added directory 'nf-stats-service/nfss/tests/unit'
1466=== added file 'nf-stats-service/nfss/tests/unit/__init__.py'
1467--- nf-stats-service/nfss/tests/unit/__init__.py 1970-01-01 00:00:00 +0000
1468+++ nf-stats-service/nfss/tests/unit/__init__.py 2014-07-01 22:22:26 +0000
1469@@ -0,0 +1,14 @@
1470+# Ubuntu CI Engine
1471+# Copyright 2014 Canonical Ltd.
1472+#
1473+# This program is free software: you can redistribute it and/or modify it
1474+# under the terms of the GNU Affero General Public License version 3, as
1475+# published by the Free Software Foundation.
1476+#
1477+# This program is distributed in the hope that it will be useful, but
1478+# WITHOUT ANY WARRANTY; without even the implied warranties of
1479+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1480+# PURPOSE. See the GNU Affero General Public License for more details.
1481+#
1482+# You should have received a copy of the GNU Affero General Public License
1483+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1484
1485=== added file 'nf-stats-service/nfss/tests/unit/test_db_migration.py'
1486--- nf-stats-service/nfss/tests/unit/test_db_migration.py 1970-01-01 00:00:00 +0000
1487+++ nf-stats-service/nfss/tests/unit/test_db_migration.py 2014-07-01 22:22:26 +0000
1488@@ -0,0 +1,93 @@
1489+# Ubuntu CI Engine
1490+# Copyright 2014 Canonical Ltd.
1491+#
1492+# This program is free software: you can redistribute it and/or modify it
1493+# under the terms of the GNU Affero General Public License version 3, as
1494+# published by the Free Software Foundation.
1495+#
1496+# This program is distributed in the hope that it will be useful, but
1497+# WITHOUT ANY WARRANTY; without even the implied warranties of
1498+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1499+# PURPOSE. See the GNU Affero General Public License for more details.
1500+#
1501+# You should have received a copy of the GNU Affero General Public License
1502+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1503+
1504+
1505+import tempfile
1506+import unittest
1507+
1508+try:
1509+ from mock import patch
1510+ have_mock = True
1511+except ImportError:
1512+ have_mock = False
1513+
1514+from nfss import db_migrate as dbm
1515+from nfss import database
1516+
1517+
1518+class DBMigrationTests(unittest.TestCase):
1519+
1520+ def setUp(self):
1521+ super().setUp()
1522+ self.addCleanup(database.get_db_settings.cache_clear)
1523+
1524+ def test_get_settings_returns_dict_on_no_file(self):
1525+ with self.assertRaises(IOError):
1526+ database.get_db_settings('/this/path/does/not/exist')
1527+
1528+ def test_get_settings_returns_dict_on_bad_json(self):
1529+ with tempfile.NamedTemporaryFile(mode='w') as f:
1530+ f.write('sdfsdf')
1531+ f.flush()
1532+
1533+ with self.assertRaises(ValueError):
1534+ database.get_db_settings(f.name)
1535+
1536+ def test_get_settings_works_on_example_json(self):
1537+ with tempfile.NamedTemporaryFile(mode='w') as f:
1538+ f.write(
1539+ '{"database": "ci-airline-nfss-restish", '
1540+ '"allowed-units": "ci-airline-nfss-restish/0", '
1541+ '"state": "standalone", '
1542+ '"schema_user": "db_2_ci_airline_nfss_restish_schema", '
1543+ '"schema_password": "secret", '
1544+ '"private-address": "10.0.3.236", '
1545+ '"host": "10.0.3.236", '
1546+ '"user": "db_2_ci_airline_nfss_restish", '
1547+ '"password": "secret", '
1548+ '"port": "5432"}'
1549+ )
1550+ f.flush()
1551+
1552+ expected = {
1553+ "database": "ci-airline-nfss-restish",
1554+ "allowed-units": "ci-airline-nfss-restish/0",
1555+ "state": "standalone",
1556+ "schema_user": "db_2_ci_airline_nfss_restish_schema",
1557+ "schema_password": "secret",
1558+ "private-address": "10.0.3.236",
1559+ "host": "10.0.3.236",
1560+ "user": "db_2_ci_airline_nfss_restish",
1561+ "password": "secret",
1562+ "port": "5432"
1563+ }
1564+ self.assertEqual(
1565+ expected,
1566+ database.get_db_settings(f.name)
1567+ )
1568+
1569+ @unittest.skipUnless(have_mock, "No Mock available")
1570+ def test_get_maximum_version_works_with_single_patch(self):
1571+ with patch.object(dbm, 'get_all_patch_paths') as p:
1572+ p.return_value = ['000-foo.sql']
1573+
1574+ self.assertEqual(0, dbm.get_maximum_version())
1575+
1576+ @unittest.skipUnless(have_mock, "No Mock available")
1577+ def test_get_maximum_version_works_with_many_patches(self):
1578+ with patch.object(dbm, 'get_all_patch_paths') as p:
1579+ p.return_value = ['000-foo.sql', '001-bar.sql', '002-baz.sql']
1580+
1581+ self.assertEqual(2, dbm.get_maximum_version())
1582
1583=== added file 'nf-stats-service/setup.py'
1584--- nf-stats-service/setup.py 1970-01-01 00:00:00 +0000
1585+++ nf-stats-service/setup.py 2014-07-01 22:22:26 +0000
1586@@ -0,0 +1,32 @@
1587+#!/usr/bin/env python
1588+# Ubuntu CI Engine
1589+# Copyright 2014 Canonical Ltd.
1590+
1591+# This program is free software: you can redistribute it and/or modify it
1592+# under the terms of the GNU Affero General Public License version 3, as
1593+# published by the Free Software Foundation.
1594+
1595+# This program is distributed in the hope that it will be useful, but
1596+# WITHOUT ANY WARRANTY; without even the implied warranties of
1597+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1598+# PURPOSE. See the GNU Affero General Public License for more details.
1599+
1600+# You should have received a copy of the GNU Affero General Public License
1601+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1602+
1603+from setuptools import setup
1604+
1605+requires = [
1606+ 'pyramid==1.5.1',
1607+ 'psycopg2==2.4.5',
1608+ 'oauthlib==0.6.1'
1609+]
1610+
1611+
1612+setup(
1613+ name='nfss',
1614+ version='0.1',
1615+ description='Non Functional Stats Service - RESTful API.',
1616+ test_suite='nfss.tests.unit',
1617+ tests_require=requires
1618+)
1619
1620=== added directory 'nf-stats-service/web_static'
1621=== added file 'nf-stats-service/web_static/app.js'
1622--- nf-stats-service/web_static/app.js 1970-01-01 00:00:00 +0000
1623+++ nf-stats-service/web_static/app.js 2014-07-01 22:22:26 +0000
1624@@ -0,0 +1,243 @@
1625+var app = angular.module('NonFunctional', ['ngAnimate', 'nvd3ChartDirectives', 'ngQuickDate']);
1626+
1627+var API_PREFIX = "/api/v1/";
1628+var GRAPH_DIR = "/graphs/";
1629+var ISO_EIGHT_SIX_OH_ONE = "%Y-%m-%dT%H:%M:%SZ";
1630+var ISO_ISH = "%Y-%m-%d %H:%M:%S %Z";
1631+
1632+// This allows reversing the display order of lists in angular templates
1633+// by doing something like ng-repeat="foo in bar | reverse"
1634+app.filter('reverse', function() {
1635+ return function(items) {
1636+ return items.slice().reverse();
1637+ };
1638+});
1639+
1640+function appController($scope, $location, $http) {
1641+ $scope.loading = false;
1642+
1643+ $scope.endDate = null;
1644+ $scope.startDate = null;
1645+
1646+ function dateWatcher(oldValue, newValue) {
1647+ if ($scope.startDate && $scope.endDate) {
1648+ $scope.url.start_date = $scope.startDate.getTime() / 1000;
1649+ $scope.url.end_date = $scope.endDate.getTime() / 1000;
1650+ $location.search($scope.url);
1651+ $scope.getGraphData();
1652+ }
1653+ }
1654+
1655+ $scope.$watch('endDate', dateWatcher);
1656+ $scope.$watch('startDate', dateWatcher);
1657+
1658+ $scope.ISO_ISH = ISO_ISH;
1659+
1660+ // Produce a date range for the REST API if appropriate
1661+ $scope.dateRange = function() {
1662+ var start = $scope.url.start_date;
1663+ var end = $scope.url.end_date;
1664+ if (start && end) {
1665+ return '?start_date=' + start + '&end_date=' + end;
1666+ }
1667+ return '';
1668+ }
1669+
1670+ // Parse URL query paramters into a dict and return it.
1671+ function URLParams() {
1672+ return $location.search();
1673+ }
1674+
1675+ // Watch for changes in the URL and apply that data to the scope.
1676+ // Values are stored as eg $scope.url.project
1677+ function URLWatcher(queryParams) {
1678+ $scope.url = queryParams;
1679+ }
1680+
1681+ $scope.$watch(URLParams, URLWatcher);
1682+
1683+ function setChosenProjectData(data) {
1684+ $scope.chosenProjectData = data.tests;
1685+ }
1686+
1687+ // When a project is chosen, get a list of its tests
1688+ function projectWatcher(chosenProject) {
1689+ $scope.tests = [];
1690+ $scope.graphData = [];
1691+ if (chosenProject) {
1692+ $scope.tests = $scope.allProjects[chosenProject].tests.sort();
1693+ $http.get($scope.allProjects[chosenProject].path).success(setChosenProjectData);
1694+ }
1695+ }
1696+
1697+ // Handle results from initial rest request: Show the list of projects.
1698+ function projectInitializer(data) {
1699+ $scope.projects = Object.keys(data.projects).sort();
1700+ $scope.allProjects = data.projects;
1701+ $scope.$watch('url.project', projectWatcher);
1702+ }
1703+
1704+ // Make the initial rest request.
1705+ $http.get(API_PREFIX).success(projectInitializer);
1706+
1707+ // 'item' refers to each entry in the blob.data array, and allows for
1708+ // graph definitions that look like y="accessor('item.delta')"
1709+ $scope.accessor = function(accessor) { return function(item) { return eval(accessor); } };
1710+
1711+ // Return a string that identifies the current chosen project + test.
1712+ $scope.slug = function() {
1713+ if ($scope.url.project && $scope.url.test) {
1714+ return $scope.url.project + '_' + $scope.url.test;
1715+ }
1716+ }
1717+
1718+ // d3 expects data in a certain format; this is the most basic possible
1719+ // format that makes no assumptions about the structure of the data coming
1720+ // from the DB. Graph definitions have the ability to override this.
1721+ $scope.defaultMassageGraphData = function(blob) {
1722+ $scope.graphData = [ { values: blob.data } ];
1723+ }
1724+
1725+ // Convert "2014-04-15 09:35:07.926+00" into a legit JS date object.
1726+ // Note, it's "parts[1] - 1" because months are 0-indexed.
1727+ $scope.dateParser = function(dateString) {
1728+ var parts = dateString.split(/[-:. TZ]/g);
1729+ return new Date(Date.UTC(parts[0], parts[1] - 1, parts[2],
1730+ parts[3], parts[4], parts[5]));
1731+ }
1732+
1733+ // Display dates nicely. For information on the format string, refer to
1734+ // https://github.com/mbostock/d3/wiki/Time-Formatting#format
1735+ $scope.dateFormatter = function(formatString) {
1736+ return function(date) {
1737+ return d3.time.format(formatString || ISO_ISH)(new Date(date));
1738+ }
1739+ }
1740+
1741+ // Display numbers nicely. For information on the format string, refer to
1742+ // https://github.com/mbostock/d3/wiki/Formatting#d3_format
1743+ $scope.numberFormatter = function(formatString, units) {
1744+ return function(number) {
1745+ return (d3.format(formatString || ',.2f')(number) + units);
1746+ }
1747+ }
1748+
1749+ // Create a new date object that is an arbitrary number of months offset
1750+ // from the passed-in date object.
1751+ function deltaMonth(date, months) {
1752+ date = new Date(date);
1753+ date.setMonth(date.getMonth() + months);
1754+ return date;
1755+ }
1756+
1757+ // Generate a URL query string that contains the appropriate date range
1758+ // defined by the arguments start and end (which should be date objects).
1759+ function makeMonthLink(start, end) {
1760+ return ('#?project=' + $scope.url.project + '&test=' + $scope.url.test +
1761+ '&start_date=' + (start.getTime() / 1000) + '&end_date=' + (end.getTime() / 1000));
1762+ }
1763+
1764+ // Set the values for the next/previous month links after the graph data has loaded.
1765+ function setMonthLinks(blob) {
1766+ $scope.loading = false;
1767+ $scope.hidePrevLink = false;
1768+ $scope.hideNextLink = false;
1769+ $scope.veryFirst = $scope.dateParser(blob.first_data);
1770+ $scope.veryLast = $scope.dateParser(blob.last_data);
1771+ var first = $scope.url.start_date ? new Date($scope.url.start_date * 1000) : $scope.dateParser(blob.data_range_first);
1772+ var last = $scope.url.end_date ? new Date($scope.url.end_date * 1000) : $scope.dateParser(blob.data_range_last);
1773+ if (first.getTime() <= $scope.veryFirst.getTime()) {
1774+ $scope.hidePrevLink = true;
1775+ }
1776+ if (last.getTime() >= $scope.veryLast.getTime()) {
1777+ $scope.hideNextLink = true;
1778+ }
1779+ $scope.prevMonthLink = makeMonthLink(deltaMonth(first, -1), first);
1780+ $scope.nextMonthLink = makeMonthLink(last, deltaMonth(last, 1));
1781+ }
1782+
1783+ // Fetch the actual graph data from the db.
1784+ $scope.getGraphData = function() {
1785+ $scope.loading = true;
1786+ $scope.graphData = [];
1787+ $http.get(API_PREFIX + $scope.url.project + '/' + $scope.url.test + $scope.dateRange())
1788+ .success(setMonthLinks)
1789+ .success($scope.massageGraphData);
1790+ }
1791+}
1792+
1793+app.directive('customGraph', function($compile, $http, $templateCache) {
1794+ // This function handles the switching of graph definitions and loading of
1795+ // graph data when the user navigates between pages. The functions defined
1796+ // within this function will make more sense if you read them bottom to top.
1797+ function graphLinker(scope, element, attrs) {
1798+ var defaultGraph = '<ol><li ng-repeat="item in graphData[0].values">' +
1799+ '<b>{{dateFormatter()(dateParser(item.date_entered))}}:</b>' +
1800+ '<br />{{item.data}}</li></ol>';
1801+
1802+ // Take a string of raw HTML data, insert it into the DOM, and
1803+ // tell angular that the DOM has changed so it can make it go.
1804+ function graphChanger(htmlData) {
1805+ element.html(htmlData || defaultGraph);
1806+ $compile(element.contents())(scope);
1807+ var script = element.contents()[0];
1808+ if (script.tagName == "SCRIPT") {
1809+ // script.text should contain a meaningful definition of
1810+ // scope.massageGraphData
1811+ eval(script.text);
1812+ }
1813+ // Now that the graph definition is ready, let's load the graph data.
1814+ scope.getGraphData();
1815+ }
1816+
1817+ // Make the AJAX request to load the raw HTML data, calling graphChanger
1818+ // on success, returns a promise so you can handle errors.
1819+ function getGraphDef(slug) {
1820+ scope.loading = true;
1821+ return $http({
1822+ method: 'GET',
1823+ url: GRAPH_DIR + slug + '.html?nocache=' + new Date().getTime(),
1824+ cache: $templateCache
1825+ }).success(graphChanger);
1826+ }
1827+
1828+ // When all else fails, display the raw data in a list.
1829+ function setGlobalDefaultGraph() {
1830+ graphChanger(defaultGraph);
1831+ }
1832+
1833+ // When there is neither a project+test specific graph definition, nor
1834+ // a generic test-only graph definition, try looking for a generic
1835+ // project-only graph definition. Eg, this loads bootspeed.html
1836+ function setProjectDefaultGraph() {
1837+ getGraphDef(scope.url.project).error(setGlobalDefaultGraph);
1838+ }
1839+
1840+ // When there is no project+test specific graph definition, look
1841+ // instead of a generic test-only graph definition which serves as a
1842+ // default for all projects that run that test. Eg, app_startup_benchmark.html
1843+ function setTestDefaultGraph() {
1844+ getGraphDef(scope.url.test).error(setProjectDefaultGraph);
1845+ }
1846+
1847+ // Attempt to load a project+test specific graph definition, this
1848+ // can be used to override the graph display on a case-by-case basis.
1849+ scope.graphWatcher = function(urlParams) {
1850+ scope.graphData = [];
1851+ scope.startDate = null;
1852+ scope.endDate = null;
1853+ var slug = scope.slug();
1854+ if (slug) {
1855+ scope.massageGraphData = scope.defaultMassageGraphData;
1856+ getGraphDef(slug).error(setTestDefaultGraph);
1857+ }
1858+ };
1859+
1860+ scope.$watch('url', scope.graphWatcher);
1861+ }
1862+
1863+ return {
1864+ replace: true,
1865+ link: graphLinker
1866+ };
1867+});
1868
1869=== added directory 'nf-stats-service/web_static/graphs'
1870=== added file 'nf-stats-service/web_static/graphs/app_startup_benchmark.html'
1871--- nf-stats-service/web_static/graphs/app_startup_benchmark.html 1970-01-01 00:00:00 +0000
1872+++ nf-stats-service/web_static/graphs/app_startup_benchmark.html 2014-07-01 22:22:26 +0000
1873@@ -0,0 +1,61 @@
1874+<script type="text/javascript">
1875+// This is a subtractor factory, that is, a function that returns a function
1876+// that does subtraction.
1877+function subtractory(minuend, subtrahend) {
1878+ return function(item) {
1879+ var difference = (item.data[0][minuend] - item.data[0][subtrahend]) / 1000000;
1880+ return { id: item.id, date_entered: scope.dateParser(item.date_entered), delta: difference || 0 };
1881+ }
1882+}
1883+
1884+scope.massageGraphData = function(blob) {
1885+ scope.graphData = [
1886+ {
1887+ key: "Callback - Sent",
1888+ values: blob.data.map(subtractory("upstart_app_launch:libual_start_message_callback",
1889+ "upstart_app_launch:libual_start_message_sent"))
1890+ },
1891+ {
1892+ key: "PreExec - Sent",
1893+ values: blob.data.map(subtractory("upstart_app_launch:exec_pre_exec",
1894+ "upstart_app_launch:libual_start_message_sent"))
1895+ },
1896+ {
1897+ key: "Callback - PreExec",
1898+ values: blob.data.map(subtractory("upstart_app_launch:libual_start_message_callback",
1899+ "upstart_app_launch:exec_pre_exec"))
1900+ }
1901+ ];
1902+}
1903+</script>
1904+
1905+<nvd3-line-chart
1906+ data="graphData"
1907+ id="app_startup_benchmark_graph"
1908+ height="400"
1909+ margin="{left:100,top:10,bottom:40,right:100}"
1910+ x="accessor('item.date_entered')"
1911+ y="accessor('item.delta')"
1912+ useInteractiveGuideLine="true"
1913+ tooltips="true"
1914+ showXAxis="true"
1915+ xAxisTickFormat="dateFormatter()"
1916+ xAxisStaggerLabels="true"
1917+ showYAxis="true"
1918+ yAxisTickFormat="numberFormatter(',.2f', 'ms')"
1919+ yAxisTickPadding="10"
1920+ showLegend="true"
1921+ showValues="true">
1922+ <svg></svg>
1923+</nvd3-line-chart>
1924+
1925+<table>
1926+ <tr ng-repeat="series in graphData">
1927+ <td><b>{{series.key}}:</b></td>
1928+ <td ng-repeat="item in series.values">{{numberFormatter(',.3f', 'ms')(item.delta)}}</td>
1929+ </tr>
1930+ <tr>
1931+ <td><b>Date Entered:</b></td>
1932+ <td ng-repeat="item in graphData[0].values">{{dateFormatter(ISO_ISH)(item.date_entered)}}</td>
1933+ </tr>
1934+</table>
1935
1936=== added file 'nf-stats-service/web_static/graphs/bootspeed.html'
1937--- nf-stats-service/web_static/graphs/bootspeed.html 1970-01-01 00:00:00 +0000
1938+++ nf-stats-service/web_static/graphs/bootspeed.html 2014-07-01 22:22:26 +0000
1939@@ -0,0 +1,66 @@
1940+<script type="text/javascript">
1941+
1942+function isolator(key) {
1943+ return function(item) {
1944+ return { date_entered: scope.dateParser(item.date_entered),
1945+ timespan: Math.max(item.data[key], 0) }
1946+ }
1947+}
1948+
1949+scope.massageGraphData = function(blob) {
1950+ scope.rawData = blob;
1951+ scope.graphData = [
1952+ {
1953+ key: 'Kernel',
1954+ values: blob.data.map(isolator('kernel'))
1955+ },
1956+ {
1957+ key: 'Plumbing',
1958+ values: blob.data.map(isolator('plumbing'))
1959+ },
1960+ {
1961+ key: 'XOrg',
1962+ values: blob.data.map(isolator('xorg'))
1963+ },
1964+ {
1965+ key: 'Desktop',
1966+ values: blob.data.map(isolator('desktop'))
1967+// },
1968+// {
1969+// key: 'Total Boot',
1970+// values: blob.data.map(isolator('boot'))
1971+ }
1972+ ];
1973+}
1974+</script>
1975+
1976+<nvd3-stacked-area-chart
1977+ data="graphData"
1978+ id="bootspeed_graph"
1979+ height="400"
1980+ margin="{left:100,top:10,bottom:40,right:100}"
1981+ x="accessor('item.date_entered')"
1982+ y="accessor('item.timespan')"
1983+ useInteractiveGuideLine="true"
1984+ tooltips="true"
1985+ showXAxis="true"
1986+ xAxisTickFormat="dateFormatter()"
1987+ xAxisStaggerLabels="true"
1988+ showYAxis="true"
1989+ yAxisTickFormat="numberFormatter(',.2f', 's')"
1990+ yAxisTickPadding="10"
1991+ showLegend="true"
1992+ showValues="true">
1993+ <svg></svg>
1994+</nvd3-stacked-area-chart>
1995+
1996+<table>
1997+ <tr ng-repeat="series in graphData | reverse">
1998+ <td><b>{{series.key}}:</b></td>
1999+ <td ng-repeat="item in series.values">{{numberFormatter(',.2f', 's')(item.timespan)}}</td>
2000+ </tr>
2001+ <tr>
2002+ <td><b>Date Entered:</b></td>
2003+ <td ng-repeat="item in graphData[0].values">{{dateFormatter(ISO_ISH)(item.date_entered)}}</td>
2004+ </tr>
2005+</table>
2006
2007=== added file 'nf-stats-service/web_static/index.html'
2008--- nf-stats-service/web_static/index.html 1970-01-01 00:00:00 +0000
2009+++ nf-stats-service/web_static/index.html 2014-07-01 22:22:26 +0000
2010@@ -0,0 +1,73 @@
2011+<!DOCTYPE html>
2012+<html>
2013+<head>
2014+<meta charset="utf-8" />
2015+<title>Non-Functional Statistics Service</title>
2016+<link rel="stylesheet" type="text/css" href="style.css">
2017+<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.css">
2018+<link rel="stylesheet" type="text/css" href="v/ng-quick-date.css" media="all" />
2019+<link rel="stylesheet" type="text/css" href="v/ng-quick-date-default-theme.css" media="all" />
2020+<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.13/angular.min.js"></script>
2021+<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.13/angular-animate.min.js"></script>
2022+<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js"></script>
2023+<script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.js"></script>
2024+<script src="v/angularjs-nvd3-directives.min.js"></script>
2025+<script src="v/ng-quick-date.min.js"></script>
2026+<script src="app.js"></script>
2027+</head>
2028+<body ng-app="NonFunctional" ng-controller="appController">
2029+
2030+<div class="spinner" ng-show="loading"></div>
2031+
2032+<table id="project_selector"
2033+ ng-class="url.project === undefined ? 'starter' : 'header'">
2034+ <tr>
2035+ <td class="left">
2036+ <a href="#"><h1>Home</h1></a>
2037+ <input type="search" ng-model="q" placeholder="Filter projects..." />
2038+ </td>
2039+ <td class="center">
2040+ <a ng-repeat="project in projects | filter:q"
2041+ ng-class="project == url.project ? 'active' : 'inactive'"
2042+ href="#?project={{project}}">{{project}}</a>
2043+ </td>
2044+ </tr>
2045+</table>
2046+
2047+<div id="test_selector" class="center">
2048+ <div ng-repeat="test in tests" class="testBubble">
2049+ <a ng-class="test == url.test ? 'active' : 'inactive'"
2050+ href="#?project={{url.project}}&test={{test}}">
2051+ {{test}}</a>
2052+ <div ng-hide="url.test" class="testBubbleDesc sample-show-hide">
2053+ {{chosenProjectData[test].data_points}} data points<br />
2054+ between {{dateFormatter()(chosenProjectData[test].first_data)}}<br />
2055+ and {{dateFormatter()(chosenProjectData[test].last_data)}}
2056+ </div>
2057+ </div>
2058+</div>
2059+
2060+<table id="date_picker" ng-show="graphData.length > 0">
2061+ <tr>
2062+ <td class="left">
2063+ <a href="{{prevMonthLink}}" ng-hide="hidePrevLink">&larr; Previous Month</a>
2064+ </td>
2065+ <td class="left">
2066+ <quick-datepicker placeholder="Set Start Date" ng-model="startDate" label-format="EEEE, MMMM d, yyyy"></quick-datepicker>
2067+ </td>
2068+ <td>
2069+ &nbsp;
2070+ </td>
2071+ <td class="right">
2072+ <quick-datepicker placeholder="Set End Date" ng-model="endDate" label-format="EEEE, MMMM d, yyyy"></quick-datepicker>
2073+ </td>
2074+ <td class="right">
2075+ <a href="{{nextMonthLink}}" ng-hide="hideNextLink">Next Month &rarr;</a>
2076+ </td>
2077+ </tr>
2078+</table>
2079+
2080+<div id="graph_container" custom-graph ng-show="graphData.length > 0" />
2081+
2082+</body>
2083+</html>
2084
2085=== added file 'nf-stats-service/web_static/style.css'
2086--- nf-stats-service/web_static/style.css 1970-01-01 00:00:00 +0000
2087+++ nf-stats-service/web_static/style.css 2014-07-01 22:22:26 +0000
2088@@ -0,0 +1,192 @@
2089+html, body {
2090+ background-color: white;
2091+ padding: 0;
2092+ margin: 0;
2093+ height: 100%;
2094+}
2095+
2096+h1 {
2097+ margin: 0;
2098+}
2099+
2100+.left {
2101+ text-align: left;
2102+ width: 0;
2103+}
2104+
2105+.center {
2106+ text-align: center;
2107+}
2108+
2109+.right {
2110+ text-align: right;
2111+ width: 0;
2112+}
2113+
2114+.active {
2115+ font-weight: bold;
2116+}
2117+
2118+.starter, .starter.starter-add-active {
2119+ height: 100%;
2120+}
2121+
2122+.starter-remove.starter-remove-active {
2123+ height: 100px;
2124+}
2125+
2126+.testBubbleDesc.ng-hide-add, .testBubbleDesc.ng-hide-remove, table, div, a {
2127+ -webkit-transition: all linear 0.5s;
2128+ -moz-transition: all linear 0.5s;
2129+ -ms-transition: all linear 0.5s;
2130+ -o-transition: all linear 0.5s;
2131+ transition: all linear 0.5s;
2132+}
2133+
2134+#graph_container table {
2135+ margin: 2em auto;
2136+ overflow: auto;
2137+ width: 100%;
2138+ display: block;
2139+}
2140+
2141+#graph_container td {
2142+ padding: 1px 10px;
2143+ /* white-space: nowrap; */
2144+ text-align: right;
2145+}
2146+
2147+#graph_container b {
2148+ white-space: nowrap;
2149+}
2150+
2151+#project_selector {
2152+ width: 100%;
2153+ margin: 0;
2154+ background-color: #f3f3f3;
2155+ color: black;
2156+}
2157+
2158+a {
2159+ text-decoration: none;
2160+ padding: 5px;
2161+ display: inline-block;
2162+ color: #dd4814;
2163+ white-space: nowrap;
2164+ overflow: hide;
2165+}
2166+
2167+a:hover {
2168+ text-decoration: underline;
2169+}
2170+
2171+#graph_container, svg {
2172+ width: 100% !important;
2173+ clear: both;
2174+}
2175+
2176+.ng-leave.ng-leave-active, .ng-move, .ng-enter {
2177+ opacity: 0;
2178+ max-width: 0;
2179+}
2180+
2181+.ng-leave, .ng-move.ng-move-active, .ng-enter.ng-enter-active {
2182+ opacity: 1;
2183+ max-width: 100px;
2184+}
2185+
2186+.ng-hide {
2187+ opacity: 0;
2188+}
2189+
2190+.testBubble {
2191+ display: inline-block;
2192+ font-size: small;
2193+ background-color: #dd4814;
2194+ color: white;
2195+ border-radius: 3px;
2196+ padding: 3px;
2197+ margin: 5px;
2198+ white-space: nowrap;
2199+}
2200+
2201+.testBubble a {
2202+ color: white;
2203+}
2204+
2205+.testBubbleDesc {
2206+ font-size: x-small;
2207+ display: block !important;
2208+}
2209+
2210+.testBubbleDesc.ng-hide-add.ng-hide-add-active,
2211+.testBubbleDesc.ng-hide-remove {
2212+ opacity: 0;
2213+ max-width: 0;
2214+ max-height: 0;
2215+}
2216+
2217+.testBubbleDesc.ng-hide-add,
2218+.testBubbleDesc.ng-hide-remove.ng-hide-remove-active {
2219+ opacity: 1;
2220+ max-width: 500px;
2221+ max-height: 100px;
2222+}
2223+
2224+#date_picker {
2225+ width: 100%;
2226+ text-align: center;
2227+ padding: 10px;
2228+}
2229+
2230+#date_picker .right {
2231+ padding-right: 5em;
2232+}
2233+
2234+#date_picker .left {
2235+ padding-left: 5em;
2236+}
2237+
2238+.spinner {
2239+ height: 100px;
2240+ width: 100px;
2241+ margin-top: -50px;
2242+ margin-left: -50px;
2243+ position: absolute;
2244+ top: 50%;
2245+ left: 50%;
2246+ -webkit-animation: rotation .6s infinite linear;
2247+ -moz-animation: rotation .6s infinite linear;
2248+ -o-animation: rotation .6s infinite linear;
2249+ animation: rotation .6s infinite linear;
2250+ border: 5px solid rgba(221,72,20,.15);
2251+ border-top: 5px solid rgba(221,72,20,1);
2252+ border-radius: 100%;
2253+}
2254+
2255+@-webkit-keyframes rotation {
2256+ from {-webkit-transform: rotate(0deg);}
2257+ to {-webkit-transform: rotate(359deg);}
2258+}
2259+
2260+@-moz-keyframes rotation {
2261+ from {-moz-transform: rotate(0deg);}
2262+ to {-moz-transform: rotate(359deg);}
2263+}
2264+
2265+@-o-keyframes rotation {
2266+ from {-o-transform: rotate(0deg);}
2267+ to {-o-transform: rotate(359deg);}
2268+}
2269+
2270+@keyframes rotation {
2271+ from {transform: rotate(0deg);}
2272+ to {transform: rotate(359deg);}
2273+}
2274+
2275+/* Hide the display of the X-axis vertical tick lines, this is a workaround
2276+ * for https://github.com/cmaurer/angularjs-nvd3-directives/issues/242 */
2277+.nv-x .tick line {
2278+ display: none !important;
2279+ opacity: 0 !important;
2280+}
2281
2282=== added directory 'nf-stats-service/web_static/v'
2283=== added file 'nf-stats-service/web_static/v/angularjs-nvd3-directives.min.js'
2284--- nf-stats-service/web_static/v/angularjs-nvd3-directives.min.js 1970-01-01 00:00:00 +0000
2285+++ nf-stats-service/web_static/v/angularjs-nvd3-directives.min.js 2014-07-01 22:22:26 +0000
2286@@ -0,0 +1,3 @@
2287+!function(){"use strict";function initializeLegendMargin(scope,attrs){var margin=scope.$eval(attrs.legendmargin)||{left:0,top:5,bottom:5,right:0};"object"!=typeof margin&&(margin={left:margin,top:margin,bottom:margin,right:margin}),scope.legendmargin=margin}function configureLegend(chart,scope,attrs){chart.legend&&attrs.showlegend&&"true"===attrs.showlegend&&(initializeLegendMargin(scope,attrs),chart.legend.margin(scope.legendmargin),chart.legend.width(void 0===attrs.legendwidth?400:+attrs.legendwidth),chart.legend.height(void 0===attrs.legendheight?20:+attrs.legendheight),chart.legend.key(void 0===attrs.legendkey?function(d){return d.key}:scope.legendkey()),chart.legend.color(void 0===attrs.legendcolor?nv.utils.defaultColor():scope.legendcolor()),chart.legend.align(void 0===attrs.legendalign?!0:"true"===attrs.legendalign),chart.legend.rightAlign(void 0===attrs.legendrightalign?!0:"true"===attrs.legendrightalign),chart.legend.updateState(void 0===attrs.legendupdatestate?!0:"true"===attrs.legendupdatestate),chart.legend.radioButtonMode(void 0===attrs.legendradiobuttonmode?!1:"true"===attrs.legendradiobuttonmode))}function processEvents(chart,scope){chart.dispatch&&(chart.dispatch.tooltipShow&&chart.dispatch.on("tooltipShow.directive",function(event){scope.$emit("tooltipShow.directive",event)}),chart.dispatch.tooltipHide&&chart.dispatch.on("tooltipHide.directive",function(event){scope.$emit("tooltipHide.directive",event)}),chart.dispatch.beforeUpdate&&chart.dispatch.on("beforeUpdate.directive",function(event){scope.$emit("beforeUpdate.directive",event)}),chart.dispatch.stateChange&&chart.dispatch.on("stateChange.directive",function(event){scope.$emit("stateChange.directive",event)}),chart.dispatch.changeState&&chart.dispatch.on("changeState.directive",function(event){scope.$emit("changeState.directive",event)})),chart.lines&&(chart.lines.dispatch.on("elementMouseover.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.lines.dispatch.on("elementMouseout.tooltip.directive",function(event){scope.$emit("elementMouseout.tooltip.directive",event)}),chart.lines.dispatch.on("elementClick.directive",function(event){scope.$emit("elementClick.directive",event)})),chart.stacked&&chart.stacked.dispatch&&(chart.stacked.dispatch.on("areaClick.toggle.directive",function(event){scope.$emit("areaClick.toggle.directive",event)}),chart.stacked.dispatch.on("tooltipShow.directive",function(event){scope.$emit("tooltipShow.directive",event)}),chart.stacked.dispatch.on("tooltipHide.directive",function(event){scope.$emit("tooltipHide.directive",event)})),chart.interactiveLayer&&(chart.interactiveLayer.elementMouseout&&chart.interactiveLayer.dispatch.on("elementMouseout.directive",function(event){scope.$emit("elementMouseout.directive",event)}),chart.interactiveLayer.elementMousemove&&chart.interactiveLayer.dispatch.on("elementMousemove.directive",function(event){scope.$emit("elementMousemove.directive",event)})),chart.discretebar&&(chart.discretebar.dispatch.on("elementMouseover.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.discretebar.dispatch.on("elementMouseout.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.discretebar.dispatch.on("elementClick.directive",function(event){scope.$emit("elementClick.directive",event)})),chart.multibar&&(chart.multibar.dispatch.on("elementMouseover.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.multibar.dispatch.on("elementMouseout.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.multibar.dispatch.on("elementClick.directive",function(event){scope.$emit("elementClick.directive",event)})),chart.pie&&(chart.pie.dispatch.on("elementMouseover.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.pie.dispatch.on("elementMouseout.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.pie.dispatch.on("elementClick.directive",function(event){scope.$emit("elementClick.directive",event)})),chart.scatter&&(chart.scatter.dispatch.on("elementMouseover.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.scatter.dispatch.on("elementMouseout.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)})),chart.bullet&&(chart.bullet.dispatch.on("elementMouseover.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)}),chart.bullet.dispatch.on("elementMouseout.tooltip.directive",function(event){scope.$emit("elementMouseover.tooltip.directive",event)})),chart.legend&&(chart.legend.dispatch.on("stateChange.legend.directive",function(event){scope.$emit("stateChange.legend.directive",event)}),chart.legend.dispatch.on("legendClick.directive",function(d,i){scope.$emit("legendClick.directive",d,i)}),chart.legend.dispatch.on("legendDblclick.directive",function(d,i){scope.$emit("legendDblclick.directive",d,i)}),chart.legend.dispatch.on("legendMouseover.directive",function(d,i){scope.$emit("legendMouseover.directive",d,i)})),chart.controls&&chart.controls.legendClick&&chart.controls.dispatch.on("legendClick.directive",function(d,i){scope.$emit("legendClick.directive",d,i)})}function configureXaxis(chart,scope,attrs){attrs.xaxisorient&&chart.xAxis.orient(attrs.xaxisorient),attrs.xaxisticks&&chart.xAxis.scale().ticks(attrs.xaxisticks),attrs.xaxistickvalues&&(Array.isArray(scope.$eval(attrs.xaxistickvalues))?chart.xAxis.tickValues(scope.$eval(attrs.xaxistickvalues)):"function"==typeof scope.xaxistickvalues()&&chart.xAxis.tickValues(scope.xaxistickvalues())),attrs.xaxisticksubdivide&&chart.xAxis.tickSubdivide(scope.xaxisticksubdivide()),attrs.xaxisticksize&&chart.xAxis.tickSize(scope.xaxisticksize()),attrs.xaxistickpadding&&chart.xAxis.tickPadding(scope.xaxistickpadding()),attrs.xaxistickformat&&chart.xAxis.tickFormat(scope.xaxistickformat()),attrs.xaxislabel&&chart.xAxis.axisLabel(attrs.xaxislabel),attrs.xaxisscale&&chart.xAxis.scale(scope.xaxisscale()),attrs.xaxisdomain&&(Array.isArray(scope.$eval(attrs.xaxisdomain))?chart.xDomain(scope.$eval(attrs.xaxisdomain)):"function"==typeof scope.xaxisdomain()&&chart.xDomain(scope.xaxisdomain())),attrs.xaxisrange&&(Array.isArray(scope.$eval(attrs.xaxisrange))?chart.xRange(scope.$eval(attrs.xaxisrange)):"function"==typeof scope.xaxisrange()&&chart.xRange(scope.xaxisrange())),attrs.xaxisrangeband&&chart.xAxis.rangeBand(scope.xaxisrangeband()),attrs.xaxisrangebands&&chart.xAxis.rangeBands(scope.xaxisrangebands()),attrs.xaxisshowmaxmin&&chart.xAxis.showMaxMin("true"===attrs.xaxisshowmaxmin),attrs.xaxishighlightzero&&chart.xAxis.highlightZero("true"===attrs.xaxishighlightzero),attrs.xaxisrotatelabels&&chart.xAxis.rotateLabels(+attrs.xaxisrotatelabels),attrs.xaxisstaggerlabels&&chart.xAxis.staggerLabels("true"===attrs.xaxisstaggerlabels),attrs.xaxislabeldistance&&chart.xAxis.axisLabelDistance(+attrs.xaxislabeldistance)}function configureX2axis(chart,scope,attrs){attrs.x2axisorient&&chart.x2Axis.orient(attrs.x2axisorient),attrs.x2axisticks&&chart.x2Axis.scale().ticks(attrs.x2axisticks),attrs.x2axistickvalues&&(Array.isArray(scope.$eval(attrs.x2axistickvalues))?chart.x2Axis.tickValues(scope.$eval(attrs.x2axistickvalues)):"function"==typeof scope.xaxistickvalues()&&chart.x2Axis.tickValues(scope.x2axistickvalues())),attrs.x2axisticksubdivide&&chart.x2Axis.tickSubdivide(scope.x2axisticksubdivide()),attrs.x2axisticksize&&chart.x2Axis.tickSize(scope.x2axisticksize()),attrs.x2axistickpadding&&chart.x2Axis.tickPadding(scope.x2axistickpadding()),attrs.x2axistickformat&&chart.x2Axis.tickFormat(scope.x2axistickformat()),attrs.x2axislabel&&chart.x2Axis.axisLabel(attrs.x2axislabel),attrs.x2axisscale&&chart.x2Axis.scale(scope.x2axisscale()),attrs.x2axisdomain&&(Array.isArray(scope.$eval(attrs.x2axisdomain))?chart.x2Axis.domain(scope.$eval(attrs.x2axisdomain)):"function"==typeof scope.x2axisdomain()&&chart.x2Axis.domain(scope.x2axisdomain())),attrs.x2axisrange&&(Array.isArray(scope.$eval(attrs.x2axisrange))?chart.x2Axis.range(scope.$eval(attrs.x2axisrange)):"function"==typeof scope.x2axisrange()&&chart.x2Axis.range(scope.x2axisrange())),attrs.x2axisrangeband&&chart.x2Axis.rangeBand(scope.x2axisrangeband()),attrs.x2axisrangebands&&chart.x2Axis.rangeBands(scope.x2axisrangebands()),attrs.x2axisshowmaxmin&&chart.x2Axis.showMaxMin("true"===attrs.x2axisshowmaxmin),attrs.x2axishighlightzero&&chart.x2Axis.highlightZero("true"===attrs.x2axishighlightzero),attrs.x2axisrotatelables&&chart.x2Axis.rotateLabels(+attrs.x2axisrotatelables),attrs.x2axisstaggerlabels&&chart.x2Axis.staggerLabels("true"===attrs.x2axisstaggerlabels),attrs.x2axislabeldistance&&chart.x2Axis.axisLabelDistance(+attrs.x2axislabeldistance)}function configureYaxis(chart,scope,attrs){attrs.yaxisorient&&chart.yAxis.orient(attrs.yaxisorient),attrs.yaxisticks&&chart.yAxis.scale().ticks(attrs.yaxisticks),attrs.yaxistickvalues&&(Array.isArray(scope.$eval(attrs.yaxistickvalues))?chart.yAxis.tickValues(scope.$eval(attrs.yaxistickvalues)):"function"==typeof scope.yaxistickvalues()&&chart.yAxis.tickValues(scope.yaxistickvalues())),attrs.yaxisticksubdivide&&chart.yAxis.tickSubdivide(scope.yaxisticksubdivide()),attrs.yaxisticksize&&chart.yAxis.tickSize(scope.yaxisticksize()),attrs.yaxistickpadding&&chart.yAxis.tickPadding(scope.yaxistickpadding()),attrs.yaxistickformat&&chart.yAxis.tickFormat(scope.yaxistickformat()),attrs.yaxislabel&&chart.yAxis.axisLabel(attrs.yaxislabel),attrs.yaxisscale&&chart.yAxis.scale(scope.yaxisscale()),attrs.yaxisdomain&&(Array.isArray(scope.$eval(attrs.yaxisdomain))?chart.yDomain(scope.$eval(attrs.yaxisdomain)):"function"==typeof scope.yaxisdomain()&&chart.yDomain(scope.yaxisdomain())),attrs.yaxisrange&&(Array.isArray(scope.$eval(attrs.yaxisrange))?chart.yRange(scope.$eval(attrs.yaxisrange)):"function"==typeof scope.yaxisrange()&&chart.yRange(scope.yaxisrange())),attrs.yaxisrangeband&&chart.yAxis.rangeBand(scope.yaxisrangeband()),attrs.yaxisrangebands&&chart.yAxis.rangeBands(scope.yaxisrangebands()),attrs.yaxisshowmaxmin&&chart.yAxis.showMaxMin("true"===attrs.yaxisshowmaxmin),attrs.yaxishighlightzero&&chart.yAxis.highlightZero("true"===attrs.yaxishighlightzero),attrs.yaxisrotatelabels&&chart.yAxis.rotateLabels(+attrs.yaxisrotatelabels),attrs.yaxisrotateylabel&&chart.yAxis.rotateYLabel("true"===attrs.yaxisrotateylabel),attrs.yaxisstaggerlabels&&chart.yAxis.staggerLabels("true"===attrs.yaxisstaggerlabels),attrs.yaxislabeldistance&&chart.yAxis.axisLabelDistance(+attrs.yaxislabeldistance)}function configureY1axis(chart,scope,attrs){attrs.y1axisticks&&chart.y1Axis.scale().ticks(attrs.y1axisticks),attrs.y1axistickvalues&&(Array.isArray(scope.$eval(attrs.y1axistickvalues))?chart.y1Axis.tickValues(scope.$eval(attrs.y1axistickvalues)):"function"==typeof scope.y1axistickvalues()&&chart.y1Axis.tickValues(scope.y1axistickvalues())),attrs.y1axisticksubdivide&&chart.y1Axis.tickSubdivide(scope.y1axisticksubdivide()),attrs.y1axisticksize&&chart.y1Axis.tickSize(scope.y1axisticksize()),attrs.y1axistickpadding&&chart.y1Axis.tickPadding(scope.y1axistickpadding()),attrs.y1axistickformat&&chart.y1Axis.tickFormat(scope.y1axistickformat()),attrs.y1axislabel&&chart.y1Axis.axisLabel(attrs.y1axislabel),attrs.y1axisscale&&chart.y1Axis.yScale(scope.y1axisscale()),attrs.y1axisdomain&&(Array.isArray(scope.$eval(attrs.y1axisdomain))?chart.y1Axis.domain(scope.$eval(attrs.y1axisdomain)):"function"==typeof scope.y1axisdomain()&&chart.y1Axis.domain(scope.y1axisdomain())),attrs.y1axisrange&&(Array.isArray(scope.$eval(attrs.y1axisrange))?chart.y1Axis.range(scope.$eval(attrs.y1axisrange)):"function"==typeof scope.y1axisrange()&&chart.y1Axis.range(scope.y1axisrange())),attrs.y1axisrangeband&&chart.y1Axis.rangeBand(scope.y1axisrangeband()),attrs.y1axisrangebands&&chart.y1Axis.rangeBands(scope.y1axisrangebands()),attrs.y1axisshowmaxmin&&chart.y1Axis.showMaxMin("true"===attrs.y1axisshowmaxmin),attrs.y1axishighlightzero&&chart.y1Axis.highlightZero("true"===attrs.y1axishighlightzero),attrs.y1axisrotatelabels&&chart.y1Axis.rotateLabels(+scope.y1axisrotatelabels),attrs.y1axisrotateylabel&&chart.y1Axis.rotateYLabel("true"===attrs.y1axisrotateylabel),attrs.y1axisstaggerlabels&&chart.y1Axis.staggerlabels("true"===attrs.y1axisstaggerlabels),attrs.y1axislabeldistance&&chart.y1Axis.axisLabelDistance(+attrs.y1axislabeldistance)}function configureY2axis(chart,scope,attrs){attrs.y2axisticks&&chart.y2Axis.scale().ticks(attrs.y2axisticks),attrs.y2axistickvalues&&chart.y2Axis.tickValues(scope.$eval(attrs.y2axistickvalues)),attrs.y2axisticksubdivide&&chart.y2Axis.tickSubdivide(scope.y2axisticksubdivide()),attrs.y2axisticksize&&chart.y2Axis.tickSize(scope.y2axisticksize()),attrs.y2axistickpadding&&chart.y2Axis.tickPadding(scope.y2axistickpadding()),attrs.y2axistickformat&&chart.y2Axis.tickFormat(scope.y2axistickformat()),attrs.y2axislabel&&chart.y2Axis.axisLabel(attrs.y2axislabel),attrs.y2axisscale&&chart.y2Axis.yScale(scope.y2axisscale()),attrs.y2axisdomain&&(Array.isArray(scope.$eval(attrs.y2axisdomain))?chart.y2Axis.domain(scope.$eval(attrs.y2axisdomain)):"function"==typeof scope.y2axisdomain()&&chart.y2Axis.domain(scope.y2axisdomain())),attrs.y2axisrange&&(Array.isArray(scope.$eval(attrs.y2axisrange))?chart.y2Axis.range(scope.$eval(attrs.y2axisrange)):"function"==typeof scope.y2axisrange()&&chart.y2Axis.range(scope.y2axisrange())),attrs.y2axisrangeband&&chart.y2Axis.rangeBand(scope.y2axisrangeband()),attrs.y2axisrangebands&&chart.y2Axis.rangeBands(scope.y2axisrangebands()),attrs.y2axisshowmaxmin&&chart.y2Axis.showMaxMin("true"===attrs.y2axisshowmaxmin),attrs.y2axishighlightzero&&chart.y2Axis.highlightZero("true"===attrs.y2axishighlightzero),attrs.y2axisrotatelabels&&chart.y2Axis.rotateLabels(+scope.y2axisrotatelabels),attrs.y2axisrotateylabel&&chart.y2Axis.rotateYLabel("true"===attrs.y2axisrotateylabel),attrs.y2axisstaggerlabels&&chart.y2Axis.staggerlabels("true"===attrs.y2axisstaggerlabels),attrs.y2axislabeldistance&&chart.y2Axis.axisLabelDistance(+attrs.y2axislabeldistance)}function initializeMargin(scope,attrs){var margin=scope.$eval(attrs.margin)||{left:50,top:50,bottom:50,right:50};"object"!=typeof margin&&(margin={left:margin,top:margin,bottom:margin,right:margin}),scope.margin=margin}function getD3Selector(attrs,element){return attrs.id?"#"+attrs.id:(attrs["data-chartid"]||angular.element(element).attrs("data-chartid","chartid"+Math.floor(1000000001*Math.random())),"[data-chartid="+attrs["data-chartid"]+"]")}function checkElementID(scope,attrs,element,chart,data){configureXaxis(chart,scope,attrs),configureX2axis(chart,scope,attrs),configureYaxis(chart,scope,attrs),configureY1axis(chart,scope,attrs),configureY2axis(chart,scope,attrs),configureLegend(chart,scope,attrs),processEvents(chart,scope);var d3Select=getD3Selector(attrs,element);angular.isArray(data)&&0===data.length&&d3.select(d3Select+" svg").remove(),d3.select(d3Select+" svg").empty()&&d3.select(d3Select).append("svg"),d3.select(d3Select+" svg").attr("height",scope.height).attr("width",scope.width).datum(data).transition().duration(void 0===attrs.transitionduration?250:+attrs.transitionduration).call(chart)}function updateDimensions(scope,attrs,element,chart){if(chart){chart.width(scope.width).height(scope.height);var d3Select=getD3Selector(attrs,element);d3.select(d3Select+" svg").attr("height",scope.height).attr("width",scope.width),nv.utils.windowResize(chart)}}angular.module("legendDirectives",[]).directive("simpleSvgLegend",function(){return{restrict:"EA",scope:{id:"@",width:"@",height:"@",margin:"@",x:"@",y:"@",labels:"@",styles:"@",classes:"@",shapes:"@",padding:"@",columns:"@"},compile:function(){return function(scope,element,attrs){var id,width,height,margin,paddingStr,svg,g,labels,styles,classes,shapes,widthTracker=0,heightTracker=0,columns=1,columnTracker=0,padding=10,svgNamespace="http://www.w3.org/2000/svg",x=0,y=0;margin=scope.$eval(attrs.margin)||{left:5,top:5,bottom:5,right:5},width="undefined"===attrs.width?element[0].parentElement.offsetWidth-(margin.left+margin.right):+attrs.width-(margin.left+margin.right),height="undefined"===attrs.height?element[0].parentElement.offsetHeight-(margin.top+margin.bottom):+attrs.height-(margin.top+margin.bottom),id=attrs.id?attrs.id:"legend-"+Math.random(),attrs.columns&&(columns=+attrs.columns),attrs.padding&&(padding=+attrs.padding),paddingStr=padding+"",svg=document.createElementNS(svgNamespace,"svg"),attrs.width&&svg.setAttribute("width",width+""),attrs.height&&svg.setAttribute("height",height+""),svg.setAttribute("id",id),attrs.x&&(x=+attrs.x),attrs.y&&(y=+attrs.y),element.append(svg),g=document.createElementNS(svgNamespace,"g"),g.setAttribute("transform","translate("+x+","+y+")"),svg.appendChild(g),attrs.labels&&(labels=scope.$eval(attrs.labels)),attrs.styles&&(styles=scope.$eval(attrs.styles)),attrs.classes&&(classes=scope.$eval(attrs.classes)),attrs.shapes&&(shapes=scope.$eval(attrs.shapes));for(var i in labels)if(labels.hasOwnProperty(i)){var shape,text,textSize,g1,shpe=shapes[i];columnTracker%columns===0&&(widthTracker=0,heightTracker+=padding+1.5*padding),g1=document.createElementNS(svgNamespace,"g"),g1.setAttribute("transform","translate("+widthTracker+", "+heightTracker+")"),"rect"===shpe?(shape=document.createElementNS(svgNamespace,"rect"),shape.setAttribute("y",0-padding/2+""),shape.setAttribute("width",paddingStr),shape.setAttribute("height",paddingStr)):"ellipse"===shpe?(shape=document.createElementNS(svgNamespace,"ellipse"),shape.setAttribute("rx",paddingStr),shape.setAttribute("ry",padding+padding/2+"")):(shape=document.createElementNS(svgNamespace,"circle"),shape.setAttribute("r",padding/2+"")),styles&&styles[i]&&shape.setAttribute("style",styles[i]),classes&&classes[i]&&shape.setAttribute("class",classes[i]),g1.appendChild(shape),widthTracker=widthTracker+shape.clientWidth+(padding+padding/2),text=document.createElementNS(svgNamespace,"text"),text.setAttribute("transform","translate(10, 5)"),text.appendChild(document.createTextNode(labels[i])),g1.appendChild(text),g.appendChild(g1),textSize=text.clientWidth,widthTracker=widthTracker+textSize+(padding+.75*padding),columnTracker++}}}}}).directive("nvd3Legend",[function(){var margin,width,height,id;return{restrict:"EA",scope:{data:"=",id:"@",margin:"&",width:"@",height:"@",key:"&",color:"&",align:"@",rightalign:"@",updatestate:"@",radiobuttonmode:"@",x:"&",y:"&"},link:function(scope,element,attrs){scope.$watch("data",function(data){if(data){if(scope.chart)return d3.select("#"+attrs.id+" svg").attr("height",height).attr("width",width).datum(data).transition().duration(250).call(scope.chart);margin=scope.$eval(attrs.margin)||{top:5,right:0,bottom:5,left:0},width=void 0===attrs.width?element[0].parentElement.offsetWidth-(margin.left+margin.right):+attrs.width-(margin.left+margin.right),height=void 0===attrs.height?element[0].parentElement.offsetHeight-(margin.top+margin.bottom):+attrs.height-(margin.top+margin.bottom),(void 0===width||0>width)&&(width=400),(void 0===height||0>height)&&(height=20),id=attrs.id?attrs.id:"legend-"+Math.random(),nv.addGraph({generate:function(){var chart=nv.models.legend().width(width).height(height).margin(margin).align(void 0===attrs.align?!0:"true"===attrs.align).rightAlign(void 0===attrs.rightalign?!0:"true"===attrs.rightalign).updateState(void 0===attrs.updatestate?!0:"true"===attrs.updatestate).radioButtonMode(void 0===attrs.radiobuttonmode?!1:"true"===attrs.radiobuttonmode).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).key(void 0===attrs.key?function(d){return d.key}:scope.key());return d3.select("#"+attrs.id+" svg")[0][0]||d3.select("#"+attrs.id).append("svg"),d3.select("#"+attrs.id+" svg").attr("height",height).attr("width",width).datum(data).transition().duration(250).call(chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart}})}})}}}]),angular.module("nvd3ChartDirectives",[]).directive("nvd3LineChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",showxaxis:"@",showyaxis:"@",rightalignyaxis:"@",defaultstate:"@",nodata:"@",margin:"&",tooltipcontent:"&",color:"&",x:"&",y:"&",forcex:"@",forcey:"@",isArea:"@",interactive:"@",clipedge:"@",clipvoronoi:"@",interpolate:"@",callback:"&",useinteractiveguideline:"@",xaxisorient:"&",xaxisticks:"@",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.lineChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceX(void 0===attrs.forcex?[]:scope.$eval(attrs.forcex)).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).showXAxis(void 0===attrs.showxaxis?!1:"true"===attrs.showxaxis).showYAxis(void 0===attrs.showyaxis?!1:"true"===attrs.showyaxis).rightAlignYAxis(void 0===attrs.rightalignyaxis?!1:"true"===attrs.rightalignyaxis).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).clipEdge(void 0===attrs.clipedge?!1:"true"===attrs.clipedge).clipVoronoi(void 0===attrs.clipvoronoi?!1:"true"===attrs.clipvoronoi).interpolate(void 0===attrs.interpolate?"linear":attrs.interpolate).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).isArea(void 0===attrs.isarea?function(d){return d.area}:function(){return"true"===attrs.isarea});return attrs.useinteractiveguideline&&chart.useInteractiveGuideline(void 0===attrs.useinteractiveguideline?!1:"true"===attrs.useinteractiveguideline),attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3CumulativeLineChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",showxaxis:"@",showyaxis:"@",rightalignyaxis:"@",defaultstate:"@",nodata:"@",margin:"&",tooltipcontent:"&",color:"&",x:"&",y:"&",forcex:"@",forcey:"@",isArea:"@",interactive:"@",clipedge:"@",clipvoronoi:"@",usevoronoi:"@",average:"&",rescaley:"@",callback:"&",useinteractiveguideline:"@",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.cumulativeLineChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceX(void 0===attrs.forcex?[]:scope.$eval(attrs.forcex)).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).showXAxis(void 0===attrs.showxaxis?!1:"true"===attrs.showxaxis).showYAxis(void 0===attrs.showyaxis?!1:"true"===attrs.showyaxis).rightAlignYAxis(void 0===attrs.rightalignyaxis?!1:"true"===attrs.rightalignyaxis).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).clipEdge(void 0===attrs.clipedge?!1:"true"===attrs.clipedge).clipVoronoi(void 0===attrs.clipvoronoi?!1:"true"===attrs.clipvoronoi).useVoronoi(void 0===attrs.usevoronoi?!1:"true"===attrs.usevoronoi).average(void 0===attrs.average?function(d){return d.average}:scope.average()).color(void 0===attrs.color?d3.scale.category10().range():scope.color()).isArea(void 0===attrs.isarea?function(d){return d.area}:"true"===attrs.isarea);return attrs.useinteractiveguideline&&chart.useInteractiveGuideline(void 0===attrs.useinteractiveguideline?!1:"true"===attrs.useinteractiveguideline),attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3StackedAreaChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",showcontrols:"@",nodata:"@",margin:"&",tooltipcontent:"&",color:"&",x:"&",y:"&",forcex:"@",forcey:"@",forcesize:"@",interactive:"@",usevoronoi:"@",clipedge:"@",interpolate:"@",style:"@",order:"@",offset:"@",size:"&",xScale:"&",yScale:"&",xDomain:"&",yDomain:"&",xRange:"&",yRange:"&",sizeDomain:"&",callback:"&",showxaxis:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",showyaxis:"&",useinteractiveguideline:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.stackedAreaChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceX(void 0===attrs.forcex?[]:scope.$eval(attrs.forcex)).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).size(void 0===attrs.size?function(d){return void 0===d.size?1:d.size}:scope.size()).forceSize(void 0===attrs.forcesize?[]:scope.$eval(attrs.forcesize)).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).showControls(void 0===attrs.showcontrols?!1:"true"===attrs.showcontrols).showXAxis(void 0===attrs.showxaxis?!1:"true"===attrs.showxaxis).showYAxis(void 0===attrs.showyaxis?!1:"true"===attrs.showyaxis).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).clipEdge(void 0===attrs.clipedge?!1:"true"===attrs.clipedge).color(void 0===attrs.color?nv.utils.defaultColor():scope.color());return attrs.useinteractiveguideline&&chart.useInteractiveGuideline(void 0===attrs.useinteractiveguideline?!1:"true"===attrs.useinteractiveguideline),attrs.usevoronoi&&chart.useVoronoi("true"===attrs.usevoronoi),attrs.style&&chart.style(attrs.style),attrs.order&&chart.order(attrs.order),attrs.offset&&chart.offset(attrs.offset),attrs.interpolate&&chart.interpolate(attrs.interpolate),attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),attrs.xscale&&chart.xScale(scope.xscale()),attrs.yscale&&chart.yScale(scope.yscale()),attrs.xdomain&&(Array.isArray(scope.$eval(attrs.xdomain))?chart.xDomain(scope.$eval(attrs.xdomain)):"function"==typeof scope.xdomain()&&chart.xDomain(scope.xdomain())),attrs.ydomain&&(Array.isArray(scope.$eval(attrs.ydomain))?chart.yDomain(scope.$eval(attrs.ydomain)):"function"==typeof scope.ydomain()&&chart.yDomain(scope.ydomain())),attrs.sizedomain&&chart.sizeDomain(scope.sizedomain()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3MultiBarChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",tooltipcontent:"&",color:"&",showcontrols:"@",nodata:"@",reducexticks:"@",staggerlabels:"@",rotatelabels:"@",margin:"&",x:"&",y:"&",forcey:"@",delay:"@",stacked:"@",callback:"&",showxaxis:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",showyaxis:"&",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)
2288+}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.multiBarChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).showControls(void 0===attrs.showcontrols?!1:"true"===attrs.showcontrols).showXAxis(void 0===attrs.showxaxis?!1:"true"===attrs.showxaxis).showYAxis(void 0===attrs.showyaxis?!1:"true"===attrs.showyaxis).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).reduceXTicks(void 0===attrs.reducexticks?!1:"true"===attrs.reducexticks).staggerLabels(void 0===attrs.staggerlabels?!1:"true"===attrs.staggerlabels).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).rotateLabels(void 0===attrs.rotatelabels?0:attrs.rotatelabels).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).delay(void 0===attrs.delay?1200:attrs.delay).stacked(void 0===attrs.stacked?!1:"true"===attrs.stacked);return attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3DiscreteBarChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",tooltips:"@",showxaxis:"@",showyaxis:"@",tooltipcontent:"&",staggerlabels:"@",color:"&",margin:"&",nodata:"@",x:"&",y:"&",forcey:"@",showvalues:"@",valueformat:"&",callback:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.discreteBarChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).showValues(void 0===attrs.showvalues?!1:"true"===attrs.showvalues).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).showXAxis(void 0===attrs.showxaxis?!1:"true"===attrs.showxaxis).showYAxis(void 0===attrs.showyaxis?!1:"true"===attrs.showyaxis).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).staggerLabels(void 0===attrs.staggerlabels?!1:"true"===attrs.staggerlabels).color(void 0===attrs.color?nv.utils.defaultColor():scope.color());return attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),attrs.valueformat&&chart.valueFormat(scope.valueformat()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3HistoricalBarChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",tooltips:"@",tooltipcontent:"&",color:"&",margin:"&",nodata:"@",x:"&",y:"&",forcey:"@",isarea:"@",interactive:"@",clipedge:"@",clipvoronoi:"@",interpolate:"@",highlightPoint:"@",clearHighlights:"@",callback:"&",useinteractiveguideline:"@",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.historicalBarChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).color(void 0===attrs.color?nv.utils.defaultColor():scope.color());return attrs.useinteractiveguideline&&chart.useInteractiveGuideline(void 0===attrs.useinteractiveguideline?!1:"true"===attrs.useinteractiveguideline),attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),attrs.valueformat&&chart.valueFormat(scope.valueformat()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3MultiBarHorizontalChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",tooltipcontent:"&",color:"&",showcontrols:"@",margin:"&",nodata:"@",x:"&",y:"&",forcey:"@",stacked:"@",showvalues:"@",valueformat:"&",callback:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.multiBarHorizontalChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).showXAxis(void 0===attrs.showxaxis?!1:"true"===attrs.showxaxis).showYAxis(void 0===attrs.showyaxis?!1:"true"===attrs.showyaxis).forceY(void 0===attrs.forcey?[0]:scope.$eval(attrs.forcey)).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).showControls(void 0===attrs.showcontrols?!1:"true"===attrs.showcontrols).showValues(void 0===attrs.showvalues?!1:"true"===attrs.showvalues).stacked(void 0===attrs.stacked?!1:"true"===attrs.stacked);return attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),attrs.valueformat&&chart.valueFormat(scope.valueformat()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3PieChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlabels:"@",showlegend:"@",donutLabelsOutside:"@",pieLabelsOutside:"@",labelType:"@",nodata:"@",margin:"&",x:"&",y:"&",color:"&",donut:"@",donutRatio:"@",labelthreshold:"@",description:"&",tooltips:"@",tooltipcontent:"&",valueFormat:"&",callback:"&",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.pieChart().x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).width(scope.width).height(scope.height).margin(scope.margin).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).showLabels(void 0===attrs.showlabels?!1:"true"===attrs.showlabels).labelThreshold(void 0===attrs.labelthreshold?.02:attrs.labelthreshold).labelType(void 0===attrs.labeltype?"key":attrs.labeltype).pieLabelsOutside(void 0===attrs.pielabelsoutside?!0:"true"===attrs.pielabelsoutside).valueFormat(void 0===attrs.valueformat?d3.format(",.2f"):attrs.valueformat).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).description(void 0===attrs.description?function(d){return d.description}:scope.description()).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).donutLabelsOutside(void 0===attrs.donutlabelsoutside?!1:"true"===attrs.donutlabelsoutside).donut(void 0===attrs.donut?!1:"true"===attrs.donut).donutRatio(void 0===attrs.donutratio?.5:attrs.donutratio);return attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3ScatterChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",showcontrols:"@",showDistX:"@",showDistY:"@",rightAlignYAxis:"@",fisheye:"@",xPadding:"@",yPadding:"@",tooltipContent:"&",tooltipXContent:"&",tooltipYContent:"&",color:"&",margin:"&",nodata:"@",transitionDuration:"@",shape:"&",onlyCircles:"@",interactive:"@",x:"&",y:"&",size:"&",forceX:"@",forceY:"@",forceSize:"@",xrange:"&",xdomain:"&",xscale:"&",yrange:"&",ydomain:"&",yscale:"&",sizerange:"&",sizedomain:"&",zscale:"&",callback:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.scatterChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d.x}:scope.x()).y(void 0===attrs.y?function(d){return d.y}:scope.y()).size(void 0===attrs.size?function(d){return void 0===d.size?1:d.size}:scope.size()).forceX(void 0===attrs.forcex?[]:scope.$eval(attrs.forcex)).forceY(void 0===attrs.forcey?[]:scope.$eval(attrs.forcey)).forceSize(void 0===attrs.forcesize?[]:scope.$eval(attrs.forcesize)).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).tooltipContent(void 0===attrs.tooltipContent?null:scope.tooltipContent()).tooltipXContent(void 0===attrs.tooltipxcontent?function(key,x){return"<strong>"+x+"</strong>"}:scope.tooltipXContent()).tooltipYContent(void 0===attrs.tooltipycontent?function(key,x,y){return"<strong>"+y+"</strong>"}:scope.tooltipYContent()).showControls(void 0===attrs.showcontrols?!1:"true"===attrs.showcontrols).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).showDistX(void 0===attrs.showdistx?!1:"true"===attrs.showdistx).showDistY(void 0===attrs.showdisty?!1:"true"===attrs.showdisty).xPadding(void 0===attrs.xpadding?0:+attrs.xpadding).yPadding(void 0===attrs.ypadding?0:+attrs.ypadding).fisheye(void 0===attrs.fisheye?0:+attrs.fisheye).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).transitionDuration(void 0===attrs.transitionduration?250:+attrs.transitionduration);return attrs.shape&&(chart.scatter.onlyCircles(!1),chart.scatter.shape(void 0===attrs.shape?function(d){return d.shape||"circle"}:scope.shape())),attrs.xdomain&&(Array.isArray(scope.$eval(attrs.xdomain))?chart.xDomain(scope.$eval(attrs.xdomain)):"function"==typeof scope.xdomain()&&chart.xDomain(scope.xdomain())),attrs.ydomain&&(Array.isArray(scope.$eval(attrs.ydomain))?chart.yDomain(scope.$eval(attrs.ydomain)):"function"==typeof scope.ydomain()&&chart.yDomain(scope.ydomain())),attrs.xscale&&(chart.xDomain(scope.xdomain()),chart.xRange(scope.xrange()),chart.xScale(scope.xscale())),attrs.yscale&&(chart.yDomain(scope.ydomain()),chart.yRange(scope.yrange()),chart.yScale(scope.yscale())),attrs.zscale&&(chart.sizeDomain(scope.sizedomain()),chart.sizeRange(scope.sizerange()),chart.zScale(scope.zscale())),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3ScatterPlusLineChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",showcontrols:"@",showDistX:"@",showDistY:"@",rightAlignYAxis:"@",fisheye:"@",tooltipContent:"&",tooltipXContent:"&",tooltipYContent:"&",color:"&",margin:"&",nodata:"@",transitionDuration:"@",shape:"&",onlyCircles:"@",interactive:"@",x:"&",y:"&",size:"&",forceX:"@",forceY:"@",forceSize:"@",xrange:"&",xdomain:"&",xscale:"&",yrange:"&",ydomain:"&",yscale:"&",sizerange:"&",sizedomain:"&",zscale:"&",callback:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.scatterPlusLineChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d.x}:scope.x()).y(void 0===attrs.y?function(d){return d.y}:scope.y()).size(void 0===attrs.size?function(d){return void 0===d.size?1:d.size}:scope.size()).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).tooltipContent(void 0===attrs.tooltipContent?null:scope.tooltipContent()).tooltipXContent(void 0===attrs.tooltipxcontent?function(key,x){return"<strong>"+x+"</strong>"}:scope.tooltipXContent()).tooltipYContent(void 0===attrs.tooltipycontent?function(key,x,y){return"<strong>"+y+"</strong>"}:scope.tooltipYContent()).showControls(void 0===attrs.showcontrols?!1:"true"===attrs.showcontrols).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).showDistX(void 0===attrs.showdistx?!1:"true"===attrs.showdistx).showDistY(void 0===attrs.showdisty?!1:"true"===attrs.showdisty).fisheye(void 0===attrs.fisheye?0:+attrs.fisheye).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).transitionDuration(void 0===attrs.transitionduration?250:+attrs.transitionduration);return attrs.shape&&(chart.scatter.onlyCircles(!1),chart.scatter.shape(void 0===attrs.shape?function(d){return d.shape||"circle"}:scope.shape())),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}})}}}]).directive("nvd3LinePlusBarChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",showlegend:"@",tooltips:"@",showxaxis:"@",showyaxis:"@",forceX:"@",forceY:"@",forceY2:"@",rightalignyaxis:"@",defaultstate:"@",nodata:"@",margin:"&",tooltipcontent:"&",color:"&",x:"&",y:"&",clipvoronoi:"@",interpolate:"@",callback:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",y1axisorient:"&",y1axisticks:"&",y1axistickvalues:"&y1axistickvalues",y1axisticksubdivide:"&",y1axisticksize:"&",y1axistickpadding:"&",y1axistickformat:"&",y1axislabel:"@",y1axisscale:"&",y1axisdomain:"&",y1axisrange:"&",y1axisrangeband:"&",y1axisrangebands:"&",y1axisshowmaxmin:"@",y1axishighlightzero:"@",y1axisrotatelabels:"@",y1axisrotateylabel:"@",y1axisstaggerlabels:"@",y1axisaxislabeldistance:"@",y2axisorient:"&",y2axisticks:"&",y2axistickvalues:"&y2axistickvalues",y2axisticksubdivide:"&",y2axisticksize:"&",y2axistickpadding:"&",y2axistickformat:"&",y2axislabel:"@",y2axisscale:"&",y2axisdomain:"&",y2axisrange:"&",y2axisrangeband:"&",y2axisrangebands:"&",y2axisshowmaxmin:"@",y2axishighlightzero:"@",y2axisrotatelabels:"@",y2axisrotateylabel:"@",y2axisstaggerlabels:"@",y2axisaxislabeldistance:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@",lineinteractive:"@",barinteractive:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.linePlusBarChart().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).interpolate(void 0===attrs.interpolate?"linear":attrs.interpolate).color(void 0===attrs.color?nv.utils.defaultColor():scope.color());return attrs.forcex&&(chart.lines.forceX(scope.$eval(attrs.forcex)),chart.bars.forceX(scope.$eval(attrs.forcex))),attrs.forcey&&(chart.lines.forceY(scope.$eval(attrs.forcey)),chart.bars.forceY(scope.$eval(attrs.forcey))),attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),attrs.lineinteractive&&"false"===attrs.lineinteractive&&chart.lines.interactive(!1),attrs.barinteractive&&"false"===attrs.barinteractive&&chart.bars.interactive(!1),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3LineWithFocusChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",height2:"@",id:"@",showlegend:"@",tooltips:"@",showxaxis:"@",showyaxis:"@",rightalignyaxis:"@",defaultstate:"@",nodata:"@",margin:"&",margin2:"&",tooltipcontent:"&",color:"&",x:"&",y:"&",forceX:"@",forceY:"@",clipedge:"@",clipvoronoi:"@",interpolate:"@",isArea:"@",size:"&",defined:"&",interactive:"@",callback:"&",xaxisorient:"&",xaxisticks:"&",xaxistickvalues:"&xaxistickvalues",xaxisticksubdivide:"&",xaxisticksize:"&",xaxistickpadding:"&",xaxistickformat:"&",xaxislabel:"@",xaxisscale:"&",xaxisdomain:"&",xaxisrange:"&",xaxisrangeband:"&",xaxisrangebands:"&",xaxisshowmaxmin:"@",xaxishighlightzero:"@",xaxisrotatelabels:"@",xaxisrotateylabel:"@",xaxisstaggerlabels:"@",xaxisaxislabeldistance:"@",x2axisorient:"&",x2axisticks:"&",x2axistickvalues:"&xaxistickvalues",x2axisticksubdivide:"&",x2axisticksize:"&",x2axistickpadding:"&",x2axistickformat:"&",x2axislabel:"@",x2axisscale:"&",x2axisdomain:"&",x2axisrange:"&",x2axisrangeband:"&",x2axisrangebands:"&",x2axisshowmaxmin:"@",x2axishighlightzero:"@",x2axisrotatelables:"@",x2axisrotateylabel:"@",x2axisstaggerlabels:"@",yaxisorient:"&",yaxisticks:"&",yaxistickvalues:"&yaxistickvalues",yaxisticksubdivide:"&",yaxisticksize:"&",yaxistickpadding:"&",yaxistickformat:"&",yaxislabel:"@",yaxisscale:"&",yaxisdomain:"&",yaxisrange:"&",yaxisrangeband:"&",yaxisrangebands:"&",yaxisshowmaxmin:"@",yaxishighlightzero:"@",yaxisrotatelabels:"@",yaxisrotateylabel:"@",yaxisstaggerlabels:"@",yaxislabeldistance:"@",y2axisorient:"&",y2axisticks:"&",y2axistickvalues:"&",y2axisticksubdivide:"&",y2axisticksize:"&",y2axistickpadding:"&",y2axistickformat:"&",y2axislabel:"@",y2axisscale:"&",y2axisdomain:"&",y2axisrange:"&",y2axisrangeband:"&",y2axisrangebands:"&",y2axisshowmaxmin:"@",y2axishighlightzero:"@",y2axisrotatelabels:"@",y2axisrotateylabel:"@",y2axisstaggerlabels:"@",legendmargin:"&",legendwidth:"@",legendheight:"@",legendkey:"@",legendcolor:"&",legendalign:"@",legendrightalign:"@",legendupdatestate:"@",legendradiobuttonmode:"@",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){if(initializeMargin(scope,attrs),attrs.margin2){var margin2=scope.$eval(attrs.margin2);"object"!=typeof margin2&&(margin2={left:margin2,top:margin2,bottom:margin2,right:margin2}),scope.margin2=margin2}else scope.margin2={top:0,right:30,bottom:20,left:60};var chart=nv.models.lineWithFocusChart().width(scope.width).height(scope.height).height2(void 0===attrs.height2?100:+attrs.height2).margin(scope.margin).margin2(scope.margin2).x(void 0===attrs.x?function(d){return d[0]}:scope.x()).y(void 0===attrs.y?function(d){return d[1]}:scope.y()).forceX(void 0===attrs.forcex?[]:scope.$eval(attrs.forcex)).forceY(void 0===attrs.forcey?[]:scope.$eval(attrs.forcey)).showLegend(void 0===attrs.showlegend?!1:"true"===attrs.showlegend).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata).color(void 0===attrs.color?nv.utils.defaultColor():scope.color()).isArea(void 0===attrs.isarea?function(d){return d.area}:function(){return"true"===attrs.isarea}).size(void 0===attrs.size?function(d){return void 0===d.size?1:d.size}:scope.size()).interactive(void 0===attrs.interactive?!1:"true"===attrs.interactive).interpolate(void 0===attrs.interpolate?"linear":attrs.interpolate);return attrs.defined&&chart.defined(scope.defined()),attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3BulletChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",margin:"&",tooltips:"@",tooltipcontent:"&",orient:"@",ranges:"&",markers:"&",measures:"&",tickformat:"&",nodata:"@",callback:"&",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.bulletChart().width(scope.width).height(scope.height).margin(scope.margin).orient(void 0===attrs.orient?"left":attrs.orient).tickFormat(void 0===attrs.tickformat?null:scope.tickformat()).tooltips(void 0===attrs.tooltips?!1:"true"===attrs.tooltips).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata);return attrs.tooltipcontent&&chart.tooltipContent(scope.tooltipcontent()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3SparklineChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",margin:"&",x:"&",y:"&",color:"&",xscale:"&",yscale:"&",showvalue:"@",alignvalue:"@",rightalignvalue:"@",nodata:"@",callback:"&",xtickformat:"&",ytickformat:"&",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){checkElementID($scope,$attrs,$element,chart,data)}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){initializeMargin(scope,attrs);var chart=nv.models.sparklinePlus().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d.x}:scope.x()).y(void 0===attrs.y?function(d){return d.y}:scope.y()).xTickFormat(void 0===attrs.xtickformat?d3.format(",r"):scope.xtickformat()).yTickFormat(void 0===attrs.ytickformat?d3.format(",.2f"):scope.ytickformat()).color(void 0===attrs.color?nv.utils.getColor(["#000"]):scope.color()).showValue(void 0===attrs.showvalue?!0:"true"===attrs.showvalue).alignValue(void 0===attrs.alignvalue?!0:"true"===attrs.alignvalue).rightAlignValue(void 0===attrs.rightalignvalue?!1:"true"===attrs.rightalignvalue).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata);return attrs.xScale&&chart.xScale(scope.xScale()),attrs.yScale&&chart.yScale(scope.yScale()),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}]).directive("nvd3SparklineWithBandlinesChart",[function(){return{restrict:"EA",scope:{data:"=",width:"@",height:"@",id:"@",margin:"&",x:"&",y:"&",color:"&",xscale:"&",yscale:"&",showvalue:"@",alignvalue:"@",rightalignvalue:"@",nodata:"@",callback:"&",xtickformat:"&",ytickformat:"&",objectequality:"@",transitionduration:"@"},controller:["$scope","$element","$attrs",function($scope,$element,$attrs){$scope.d3Call=function(data,chart){var dataAttributeChartID,selectedChart,sLineSelection,bandlineData,bandLines;$attrs.id?(d3.select("#"+$attrs.id+" svg")||d3.select("#"+$attrs.id).append("svg"),selectedChart=d3.select("#"+$attrs.id+" svg").attr("height",$scope.height).attr("width",$scope.width).datum(data),sLineSelection=d3.select("svg#"+$attrs.id+" g.nvd3.nv-wrap.nv-sparkline"),bandlineData=[$scope.bandlineProperties.min,$scope.bandlineProperties.twentyFithPercentile,$scope.bandlineProperties.median,$scope.bandlineProperties.seventyFithPercentile,$scope.bandlineProperties.max],bandLines=sLineSelection.selectAll(".nv-bandline").data([bandlineData]),bandLines.enter().append("g").attr("class","nv-bandline"),selectedChart.transition().duration(void 0===$attrs.transitionduration?250:+$attrs.transitionduration).call(chart)):(dataAttributeChartID="chartid"+Math.floor(1000000001*Math.random()),angular.element($element).attr("data-chartid",dataAttributeChartID),selectedChart=d3.select("[data-iem-chartid="+dataAttributeChartID+"] svg").attr("height",$scope.height).attr("width",$scope.width).datum(data),sLineSelection=d3.select("[data-iem-chartid="+dataAttributeChartID+"] svg g.nvd3.nv-wrap.nv-sparkline"),bandlineData=[$scope.bandlineProperties.min,$scope.bandlineProperties.twentyFithPercentile,$scope.bandlineProperties.median,$scope.bandlineProperties.seventyFithPercentile,$scope.bandlineProperties.max],bandLines=sLineSelection.selectAll(".nv-bandline").data([bandlineData]),bandLines.enter().append("g").attr("class","nv-bandline"),selectedChart.transition().duration(void 0===$attrs.transitionduration?250:+$attrs.transitionduration).call(chart))
2289+}}],link:function(scope,element,attrs){scope.$watch("width + height",function(){updateDimensions(scope,attrs,element,scope.chart)}),scope.$watch("data",function(data){if(data){if(scope.chart)return scope.d3Call(data,scope.chart);nv.addGraph({generate:function(){scope.bandlineProperties={};var sortedValues;initializeMargin(scope,attrs);var chart=nv.models.sparklinePlus().width(scope.width).height(scope.height).margin(scope.margin).x(void 0===attrs.x?function(d){return d.x}:scope.x()).y(void 0===attrs.y?function(d){return d.y}:scope.y()).xTickFormat(void 0===attrs.xtickformat?d3.format(",r"):scope.xtickformat()).yTickFormat(void 0===attrs.ytickformat?d3.format(",.2f"):scope.ytickformat()).color(void 0===attrs.color?nv.utils.getColor(["#000"]):scope.color()).showValue(void 0===attrs.showvalue?!0:"true"===attrs.showvalue).alignValue(void 0===attrs.alignvalue?!0:"true"===attrs.alignvalue).rightAlignValue(void 0===attrs.rightalignvalue?!1:"true"===attrs.rightalignvalue).noData(void 0===attrs.nodata?"No Data Available.":scope.nodata);return scope.bandlineProperties.min=d3.min(data,function(d){return d[1]}),scope.bandlineProperties.max=d3.max(data,function(d){return d[1]}),sortedValues=data.map(function(d){return d[1]}).sort(function(a,b){return a[0]<b[0]?-1:a[0]===b[0]?0:1}),scope.bandlineProperties.twentyFithPercentile=d3.quantile(sortedValues,.25),scope.bandlineProperties.median=d3.median(sortedValues),scope.bandlineProperties.seventyFithPercentile=d3.quantile(sortedValues,.75),attrs.xScale&&chart.xScale(scope.xScale()),attrs.yScale&&chart.yScale(scope.yScale()),configureXaxis(chart,scope,attrs),configureYaxis(chart,scope,attrs),processEvents(chart,scope),scope.d3Call(data,chart),nv.utils.windowResize(chart.update),scope.chart=chart,chart},callback:void 0===attrs.callback?null:scope.callback()})}},void 0===attrs.objectequality?!1:"true"===attrs.objectequality)}}}])}();
2290\ No newline at end of file
2291
2292=== added file 'nf-stats-service/web_static/v/ng-quick-date-default-theme.css'
2293--- nf-stats-service/web_static/v/ng-quick-date-default-theme.css 1970-01-01 00:00:00 +0000
2294+++ nf-stats-service/web_static/v/ng-quick-date-default-theme.css 2014-07-01 22:22:26 +0000
2295@@ -0,0 +1,104 @@
2296+.quickdate {
2297+ display: inline-block;
2298+ vertical-align: bottom;
2299+ font-size: 15px;
2300+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
2301+}
2302+.quickdate input,
2303+.quickdate select {
2304+ font-size: 13px;
2305+}
2306+.quickdate-button {
2307+ background: #ffffff;
2308+ color: #333333;
2309+ border: solid 1px #cccccc;
2310+ box-shadow: outset 0 1px 1px rgba(0, 0, 0, 0.075);
2311+ border-radius: 4px;
2312+ padding: 4px 8px;
2313+ display: inline-block;
2314+ text-decoration: none;
2315+}
2316+.quickdate-button:hover {
2317+ text-decoration: underline;
2318+}
2319+.quickdate-button:hover i {
2320+ text-decoration: none;
2321+}
2322+.quickdate-button i {
2323+ padding-right: 4px;
2324+}
2325+.quickdate-popup {
2326+ color: #333333;
2327+ font-size: 15px;
2328+ background-color: #fafafa;
2329+ border: solid 1px #dddddd;
2330+ border-radius: 3px;
2331+ -webkit-box-shadow: 0px 10px 30px rgba(25, 25, 25, 0.92);
2332+ -moz-box-shadow: 0px 10px 30px rgba(25, 25, 25, 0.92);
2333+ box-shadow: 0px 10px 30px rgba(25, 25, 25, 0.92);
2334+}
2335+.quickdate-action-link:visited,
2336+.quickdate-action-link:hover {
2337+ color: #333333;
2338+}
2339+.quickdate-next-month i {
2340+ padding-left: 10px;
2341+}
2342+.quickdate-prev-month i {
2343+ padding-right: 10px;
2344+}
2345+table.quickdate-calendar {
2346+ border: solid 1px #ccc;
2347+ background-color: #ffffff;
2348+}
2349+table.quickdate-calendar th,
2350+table.quickdate-calendar td {
2351+ border-right: 1px solid #ccc;
2352+ border-bottom: 1px solid #ccc;
2353+}
2354+table.quickdate-calendar td:hover {
2355+ background-color: #e6e6e6;
2356+}
2357+table.quickdate-calendar td.other-month {
2358+ background-color: #dbdbdb;
2359+ color: #808080;
2360+}
2361+table.quickdate-calendar td.other-month:hover {
2362+ background-color: #c7c7c7;
2363+}
2364+table.quickdate-calendar td.disabled-date {
2365+ background-color: inherit;
2366+ color: #ffffff;
2367+}
2368+table.quickdate-calendar td.disabled-date:hover {
2369+ background-color: inherit;
2370+ cursor: default;
2371+}
2372+table.quickdate-calendar td.selected {
2373+ background-color: #b0ccde;
2374+ font-weight: bold;
2375+}
2376+table.quickdate-calendar td.is-today {
2377+ color: #b58922;
2378+ font-weight: bold;
2379+}
2380+table.quickdate-calendar td.is-today.disabled-date {
2381+ color: #929292;
2382+ font-weight: normal;
2383+}
2384+.quickdate-popup-footer {
2385+ margin: 3px 1px 0;
2386+}
2387+.quickdate-clear {
2388+ display: inline-block;
2389+ padding: 2px 4px;
2390+ background-color: #ffffff;
2391+ color: #333333;
2392+ border: solid 1px #cccccc;
2393+ box-shadow: outset 0 1px 1px rgba(0, 0, 0, 0.075);
2394+ border-radius: 4px;
2395+ text-decoration: none;
2396+}
2397+.quickdate-clear:hover {
2398+ background-color: #f2f2f2;
2399+}
2400
2401=== added file 'nf-stats-service/web_static/v/ng-quick-date.css'
2402--- nf-stats-service/web_static/v/ng-quick-date.css 1970-01-01 00:00:00 +0000
2403+++ nf-stats-service/web_static/v/ng-quick-date.css 2014-07-01 22:22:26 +0000
2404@@ -0,0 +1,90 @@
2405+.quickdate {
2406+ display: inline-block;
2407+ position: relative;
2408+}
2409+.quickdate-button div,
2410+.quickdate-action-link div {
2411+ display: inline;
2412+}
2413+.quickdate-popup {
2414+ z-index: 10;
2415+ background-color: #fff;
2416+ border: solid 1px #000;
2417+ text-align: center;
2418+ width: 250px;
2419+ display: none;
2420+ position: absolute;
2421+ padding: 5px;
2422+}
2423+.quickdate-popup.open {
2424+ display: block;
2425+}
2426+.quickdate-close {
2427+ position: absolute;
2428+ top: 5px;
2429+ right: 5px;
2430+ color: #333;
2431+ font-size: 110%;
2432+ margin-top: -6px;
2433+ text-decoration: none;
2434+}
2435+.quickdate-close:hover {
2436+ text-decoration: underline;
2437+}
2438+.quickdate-close:hover,
2439+.quickdate-close:visited {
2440+ color: #333;
2441+}
2442+.quickdate-calendar-header {
2443+ display: block;
2444+ padding: 2px 0;
2445+ margin-bottom: 5px;
2446+ text-align: center;
2447+}
2448+.quickdate-month {
2449+ display: inline-block;
2450+}
2451+a.quickdate-prev-month {
2452+ float: left;
2453+}
2454+a.quickdate-next-month {
2455+ float: right;
2456+}
2457+.quickdate-text-inputs {
2458+ text-align: left;
2459+ margin-bottom: 5px;
2460+}
2461+.quickdate-input-wrapper {
2462+ width: 48%;
2463+ display: inline-block;
2464+}
2465+input.quickdate-date-input,
2466+input.quickdate-time-input {
2467+ width: 100px;
2468+ margin: 0;
2469+ height: auto;
2470+ padding: 2px 3px;
2471+}
2472+table.quickdate-calendar {
2473+ border-collapse: collapse;
2474+ border-spacing: 0;
2475+ width: 100%;
2476+ margin-top: 5px;
2477+}
2478+table.quickdate-calendar th,
2479+table.quickdate-calendar td {
2480+ padding: 5px;
2481+}
2482+table.quickdate-calendar td:hover {
2483+ cursor: pointer;
2484+}
2485+.quickdate-popup-footer {
2486+ text-align: right;
2487+ display: block;
2488+}
2489+.quickdate input.ng-invalid {
2490+ border: 1px solid #dd3b30;
2491+}
2492+.quickdate input.ng-invalid:focus {
2493+ outline-color: #dd3b30;
2494+}
2495
2496=== added file 'nf-stats-service/web_static/v/ng-quick-date.min.js'
2497--- nf-stats-service/web_static/v/ng-quick-date.min.js 1970-01-01 00:00:00 +0000
2498+++ nf-stats-service/web_static/v/ng-quick-date.min.js 2014-07-01 22:22:26 +0000
2499@@ -0,0 +1,1 @@
2500+(function(){var a;a=angular.module("ngQuickDate",[]),a.provider("ngQuickDateDefaults",function(){return{options:{dateFormat:"M/d/yyyy",timeFormat:"h:mm a",labelFormat:null,placeholder:"Click to Set Date",hoverText:null,buttonIconHtml:null,closeButtonHtml:"&times;",nextLinkHtml:"Next &rarr;",prevLinkHtml:"&larr; Prev",disableTimepicker:!1,disableClearButton:!1,defaultTime:null,dayAbbreviations:["Su","M","Tu","W","Th","F","Sa"],dateFilter:null,parseDateFunction:function(a){var b;return b=Date.parse(a),isNaN(b)?null:new Date(b)}},$get:function(){return this.options},set:function(a,b){var c,d,e;if("object"==typeof a){e=[];for(c in a)d=a[c],e.push(this.options[c]=d);return e}return this.options[a]=b}}}),a.directive("quickDatepicker",["ngQuickDateDefaults","$filter","$sce",function(a,b,c){return{restrict:"E",require:"?ngModel",scope:{dateFilter:"=?",onChange:"&",required:"@"},replace:!0,link:function(d,e,f,g){var h,i,j,k,l,m,n,o,p,q,r,s,t;return m=function(){return q(),d.toggleCalendar(!1),d.weeks=[],d.inputDate=null,d.inputTime=null,d.invalid=!0,"string"==typeof f.initValue&&g.$setViewValue(f.initValue),p(),o()},q=function(){var b,e;for(b in a)e=a[b],b.match(/[Hh]tml/)?d[b]=c.trustAsHtml(a[b]||""):!d[b]&&f[b]?d[b]=f[b]:d[b]||(d[b]=a[b]);return d.labelFormat||(d.labelFormat=d.dateFormat,d.disableTimepicker||(d.labelFormat+=" "+d.timeFormat)),f.iconClass&&f.iconClass.length?d.buttonIconHtml=c.trustAsHtml("<i ng-show='iconClass' class='"+f.iconClass+"'></i>"):void 0},i=!1,window.document.addEventListener("click",function(){return d.calendarShown&&!i&&(d.toggleCalendar(!1),d.$apply()),i=!1}),angular.element(e[0])[0].addEventListener("click",function(){return i=!0}),o=function(){var a;return a=g.$modelValue?new Date(g.$modelValue):null,s(),r(a),d.mainButtonStr=a?b("date")(a,d.labelFormat):d.placeholder,d.invalid=g.$invalid},r=function(a){return null!=a?(d.inputDate=b("date")(a,d.dateFormat),d.inputTime=b("date")(a,d.timeFormat)):(d.inputDate=null,d.inputTime=null)},p=function(a){var b;return null==a&&(a=null),b=null!=a?new Date(a):new Date,"Invalid Date"===b.toString()&&(b=new Date),b.setDate(1),d.calendarDate=new Date(b)},s=function(){var a,b,c,e,f,h,i,k,m,n,o,p,q,r;for(h=d.calendarDate.getDay(),e=l(d.calendarDate.getFullYear(),d.calendarDate.getMonth()),f=Math.ceil((h+e)/7),o=[],a=new Date(d.calendarDate),a.setDate(a.getDate()+-1*h),i=p=0,r=f-1;r>=0?r>=p:p>=r;i=r>=0?++p:--p)for(o.push([]),c=q=0;6>=q;c=++q)b=new Date(a),d.defaultTime&&(m=d.defaultTime.split(":"),b.setHours(m[0]||0),b.setMinutes(m[1]||0),b.setSeconds(m[2]||0)),k=g.$modelValue&&b&&j(b,g.$modelValue),n=j(b,new Date),o[i].push({date:b,selected:k,disabled:"function"==typeof d.dateFilter?!d.dateFilter(b):!1,other:b.getMonth()!==d.calendarDate.getMonth(),today:n}),a.setDate(a.getDate()+1);return d.weeks=o},g.$parsers.push(function(a){return d.required&&null==a?(g.$setValidity("required",!1),null):angular.isDate(a)?(g.$setValidity("required",!0),a):angular.isString(a)?(g.$setValidity("required",!0),d.parseDateFunction(a)):null}),g.$formatters.push(function(a){return angular.isDate(a)?a:angular.isString(a)?d.parseDateFunction(a):void 0}),h=function(a,c){return b("date")(a,c)},t=function(a){return"string"==typeof a?n(a):a},n=a.parseDateFunction,j=function(a,b,c){return null==c&&(c=!1),c?a-b===0:(a=t(a),b=t(b),a&&b&&a.getYear()===b.getYear()&&a.getMonth()===b.getMonth()&&a.getDate()===b.getDate())},k=function(a,b){return a&&b?parseInt(a.getTime()/6e4)===parseInt(b.getTime()/6e4):!1},l=function(a,b){return[31,a%4===0&&a%100!==0||a%400===0?29:28,31,30,31,30,31,31,30,31,30,31][b]},g.$render=function(){return p(g.$viewValue),o()},g.$viewChangeListeners.unshift(function(){return p(g.$viewValue),o(),d.onChange?d.onChange():void 0}),d.$watch("calendarShown",function(a){var b;return a?(b=angular.element(e[0].querySelector(".quickdate-date-input"))[0],b.select()):void 0}),d.toggleCalendar=function(a){return d.calendarShown=isFinite(a)?a:!d.calendarShown},d.selectDate=function(a,b){var c;return null==b&&(b=!0),c=!g.$viewValue&&a||g.$viewValue&&!a||a&&g.$viewValue&&a.getTime()!==g.$viewValue.getTime(),"function"!=typeof d.dateFilter||d.dateFilter(a)?(g.$setViewValue(a),b&&d.toggleCalendar(!1),!0):!1},d.selectDateFromInput=function(a){var b,c,e,f;null==a&&(a=!1);try{if(c=n(d.inputDate),!c)throw"Invalid Date";if(!d.disableTimepicker&&d.inputTime&&d.inputTime.length&&c){if(f=d.disableTimepicker?"00:00:00":d.inputTime,e=n(""+d.inputDate+" "+f),!e)throw"Invalid Time";c=e}if(!k(g.$viewValue,c)&&!d.selectDate(c,!1))throw"Invalid Date";return a&&d.toggleCalendar(!1),d.inputDateErr=!1,d.inputTimeErr=!1}catch(h){if(b=h,"Invalid Date"===b)return d.inputDateErr=!0;if("Invalid Time"===b)return d.inputTimeErr=!0}},d.onDateInputTab=function(){return d.disableTimepicker&&d.toggleCalendar(!1),!0},d.onTimeInputTab=function(){return d.toggleCalendar(!1),!0},d.nextMonth=function(){return p(new Date(new Date(d.calendarDate).setMonth(d.calendarDate.getMonth()+1))),o()},d.prevMonth=function(){return p(new Date(new Date(d.calendarDate).setMonth(d.calendarDate.getMonth()-1))),o()},d.clear=function(){return d.selectDate(null,!0)},m()},template:"<div class='quickdate'>\n <a href='' ng-focus='toggleCalendar()' ng-click='toggleCalendar()' class='quickdate-button' title='{{hoverText}}'><div ng-hide='iconClass' ng-bind-html='buttonIconHtml'></div>{{mainButtonStr}}</a>\n <div class='quickdate-popup' ng-class='{open: calendarShown}'>\n <a href='' tabindex='-1' class='quickdate-close' ng-click='toggleCalendar()'><div ng-bind-html='closeButtonHtml'></div></a>\n <div class='quickdate-text-inputs'>\n <div class='quickdate-input-wrapper'>\n <label>Date</label>\n <input class='quickdate-date-input' ng-class=\"{'ng-invalid': inputDateErr}\" name='inputDate' type='text' ng-model='inputDate' placeholder='1/1/2013' ng-enter=\"selectDateFromInput(true)\" ng-blur=\"selectDateFromInput(false)\" on-tab='onDateInputTab()' />\n </div>\n <div class='quickdate-input-wrapper' ng-hide='disableTimepicker'>\n <label>Time</label>\n <input class='quickdate-time-input' ng-class=\"{'ng-invalid': inputTimeErr}\" name='inputTime' type='text' ng-model='inputTime' placeholder='12:00 PM' ng-enter=\"selectDateFromInput(true)\" ng-blur=\"selectDateFromInput(false)\" on-tab='onTimeInputTab()'>\n </div>\n </div>\n <div class='quickdate-calendar-header'>\n <a href='' class='quickdate-prev-month quickdate-action-link' tabindex='-1' ng-click='prevMonth()'><div ng-bind-html='prevLinkHtml'></div></a>\n <span class='quickdate-month'>{{calendarDate | date:'MMMM yyyy'}}</span>\n <a href='' class='quickdate-next-month quickdate-action-link' ng-click='nextMonth()' tabindex='-1' ><div ng-bind-html='nextLinkHtml'></div></a>\n </div>\n <table class='quickdate-calendar'>\n <thead>\n <tr>\n <th ng-repeat='day in dayAbbreviations'>{{day}}</th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat='week in weeks'>\n <td ng-mousedown='selectDate(day.date, true, true)' ng-class='{\"other-month\": day.other, \"disabled-date\": day.disabled, \"selected\": day.selected, \"is-today\": day.today}' ng-repeat='day in week'>{{day.date | date:'d'}}</td>\n </tr>\n </tbody>\n </table>\n <div class='quickdate-popup-footer'>\n <a href='' class='quickdate-clear' tabindex='-1' ng-hide='disableClearButton' ng-click='clear()'>Clear</a>\n </div>\n </div>\n</div>"}}]),a.directive("ngEnter",function(){return function(a,b,c){return b.bind("keydown keypress",function(b){return 13===b.which?(a.$apply(c.ngEnter),b.preventDefault()):void 0})}}),a.directive("onTab",function(){return{restrict:"A",link:function(a,b,c){return b.bind("keydown keypress",function(b){return 9!==b.which||b.shiftKey?void 0:a.$apply(c.onTab)})}}})}).call(this);
2501\ No newline at end of file

Subscribers

People subscribed via source and target branches

to all changes: