datascope 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +46 -0
- data/Procfile +2 -0
- data/Rakefile +1 -0
- data/Readme.md +40 -0
- data/app.rb +74 -0
- data/config.ru +7 -0
- data/datascope.gemspec +30 -0
- data/javascripts/client.coffee +55 -0
- data/lib/stat_collector.rb +53 -0
- data/public/css/style.css +103 -0
- data/public/javascripts/backbone-min.js +38 -0
- data/public/javascripts/cubism.v1.js +1045 -0
- data/public/javascripts/d3.v2.js +7033 -0
- data/public/javascripts/underscore-min.js +5 -0
- data/schema.sql +3 -0
- data/views/index.haml +16 -0
- data/worker.rb +9 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f7decf328918f177ab900bd6a52989a5ed11f943
|
4
|
+
data.tar.gz: 570c849040549da1cad3ada61091d45175fa3fc6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3848caecfa5d7abf4d07b686baedb00d14e0939d7751980e1175e432832da598b4af3afa8b3480ffcedff98752609a7e120c55d9f595e79f01b4b77a329c27e4
|
7
|
+
data.tar.gz: eb581fbd5fff872631cdda1b58f95efbb60a21e37ccdf57aac50bf5d6edf8577553cde8398e3a41dcad98b2495002a0403913c9d60498124c632b92d3e83b83b
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.env
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
datascope (0.0.1)
|
5
|
+
haml
|
6
|
+
pg
|
7
|
+
puma
|
8
|
+
rack-coffee
|
9
|
+
sequel
|
10
|
+
sequel_pg
|
11
|
+
sinatra
|
12
|
+
|
13
|
+
GEM
|
14
|
+
remote: https://rubygems.org/
|
15
|
+
specs:
|
16
|
+
coffee-script (2.2.0)
|
17
|
+
coffee-script-source
|
18
|
+
execjs
|
19
|
+
coffee-script-source (1.7.0)
|
20
|
+
execjs (2.0.2)
|
21
|
+
haml (4.0.5)
|
22
|
+
tilt
|
23
|
+
pg (0.17.1)
|
24
|
+
puma (2.8.1)
|
25
|
+
rack (>= 1.1, < 2.0)
|
26
|
+
rack (1.5.2)
|
27
|
+
rack-coffee (1.0.2)
|
28
|
+
coffee-script
|
29
|
+
rack
|
30
|
+
rack-protection (1.5.2)
|
31
|
+
rack
|
32
|
+
sequel (4.8.0)
|
33
|
+
sequel_pg (1.6.9)
|
34
|
+
pg (>= 0.8.0)
|
35
|
+
sequel (>= 3.39.0)
|
36
|
+
sinatra (1.4.4)
|
37
|
+
rack (~> 1.4)
|
38
|
+
rack-protection (~> 1.4)
|
39
|
+
tilt (~> 1.3, >= 1.3.4)
|
40
|
+
tilt (1.4.1)
|
41
|
+
|
42
|
+
PLATFORMS
|
43
|
+
ruby
|
44
|
+
|
45
|
+
DEPENDENCIES
|
46
|
+
datascope!
|
data/Procfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/Readme.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Datascope
|
2
|
+
Visability into your Postgres 9.2 database via [pg_stat_statements](http://www.postgresql.org/docs/9.2/static/pgstatstatements.html) and [cubism](http://square.github.com/cubism/) and using the [json datatype](http://wiki.postgresql.org/wiki/What's_new_in_PostgreSQL_9.2#JSON_datatype).
|
3
|
+
|
4
|
+

|
5
|
+
|
6
|
+
Check out a [live example](https://datascope.herokuapp.com)
|
7
|
+
|
8
|
+
# Heroku Deploy
|
9
|
+
|
10
|
+
Datascope needs two Postgres 9.2 databases. The first is a DATABASE_URL with the datascope schema, the second is a TARGET_DB with the pg_stat_statements extension.
|
11
|
+
|
12
|
+
```bash
|
13
|
+
$ heroku create
|
14
|
+
|
15
|
+
$ heroku addons:add heroku-postgresql:dev --version=9.2
|
16
|
+
Attached as HEROKU_POSTGRESQL_COPPER_URL
|
17
|
+
$ heroku config:add DATABASE_URL=$(heroku config:get HEROKU_POSTGRESQL_COPPER_URL)
|
18
|
+
$ heroku pg:psql COPPER
|
19
|
+
=> \i schema.sql
|
20
|
+
CREATE TABLE
|
21
|
+
|
22
|
+
$ heroku addons:add heroku-postgresql:dev --version=9.2
|
23
|
+
Attached as HEROKU_POSTGRESQL_GREEN_URL
|
24
|
+
|
25
|
+
$ heroku config:add TARGET_DB=$(heroku config:get HEROKU_POSTGRESQL_GREEN_URL)
|
26
|
+
$ heroku pg:psql GREEN
|
27
|
+
=> create extension pg_stat_statements;
|
28
|
+
CREATE EXTENSION
|
29
|
+
|
30
|
+
$ git push heroku master
|
31
|
+
$ heroku scale worker=1
|
32
|
+
```
|
33
|
+
|
34
|
+
# Basic Auth
|
35
|
+
|
36
|
+
If you don't want your deployment of datascope to be publicly visible, simply add environment variables for `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD`.
|
37
|
+
|
38
|
+
```
|
39
|
+
heroku config:add BASIC_AUTH_USER=admin BASIC_AUTH_PASSWORD=password
|
40
|
+
```
|
data/app.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'sequel'
|
3
|
+
require 'json'
|
4
|
+
require 'haml'
|
5
|
+
DB = Sequel.connect ENV['DATABASE_URL']
|
6
|
+
|
7
|
+
class Datascope < Sinatra::Application
|
8
|
+
|
9
|
+
if (ENV['BASIC_AUTH_USER'] && ENV['BASIC_AUTH_PASSWORD'])
|
10
|
+
use Rack::Auth::Basic, "Datascope" do |username, password|
|
11
|
+
username == ENV['BASIC_AUTH_USER'] && password == ENV['BASIC_AUTH_PASSWORD']
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
get '/' do
|
16
|
+
haml :index
|
17
|
+
end
|
18
|
+
|
19
|
+
get '/metric' do
|
20
|
+
selector = params[:selector]
|
21
|
+
start = DateTime.parse params[:start]
|
22
|
+
stop = DateTime.parse params[:stop]
|
23
|
+
step = params[:step].to_i
|
24
|
+
|
25
|
+
results = DB[:stats].select(:data).filter(created_at: (start..stop)).all
|
26
|
+
parsed = results.map{|row| JSON.parse(row[:data])}
|
27
|
+
|
28
|
+
if selector == 'query_1'
|
29
|
+
values = values_by_regex parsed, /with packed/i, :ms
|
30
|
+
elsif selector == 'select'
|
31
|
+
values = values_by_regex parsed, /select/i
|
32
|
+
elsif selector == 'select_ms'
|
33
|
+
values = values_by_regex parsed, /select/i, :ms
|
34
|
+
elsif selector == 'update'
|
35
|
+
values = values_by_regex parsed, /update/i
|
36
|
+
elsif selector == 'update_ms'
|
37
|
+
values = values_by_regex parsed, /update/i, :ms
|
38
|
+
elsif selector == 'insert'
|
39
|
+
values = values_by_regex parsed, /insert/i
|
40
|
+
elsif selector == 'insert_ms'
|
41
|
+
values = values_by_regex parsed, /insert/i, :ms
|
42
|
+
elsif selector == 'delete'
|
43
|
+
values = values_by_regex parsed, /delete/i
|
44
|
+
else
|
45
|
+
values = parsed.map{|d| d[selector] }
|
46
|
+
end
|
47
|
+
|
48
|
+
JSON.dump values
|
49
|
+
end
|
50
|
+
|
51
|
+
get '/queries' do
|
52
|
+
data = JSON.parse DB[:stats]
|
53
|
+
.select(:data)
|
54
|
+
.order_by(:id.desc)
|
55
|
+
.first[:data]
|
56
|
+
JSON.dump data['stat_statements'].map{|s| s['query'] = s['query'][0..50]; s}.sort{|s| s['total_time']}.reverse
|
57
|
+
end
|
58
|
+
|
59
|
+
def values_by_regex(parsed, regex, ms=false)
|
60
|
+
vals = parsed.map { |row|
|
61
|
+
row['stat_statements']
|
62
|
+
.select {|h| h['query'] =~ regex }
|
63
|
+
.sort_by {|h| h['total_time']}
|
64
|
+
.inject([0,0]) { |m,h| [m.first + h['calls'].to_i, m.last + h['total_time'].to_f ] }
|
65
|
+
}
|
66
|
+
|
67
|
+
if ms
|
68
|
+
vals.map {|pair| pair.first.zero? ? 0 : pair.last/pair.first}
|
69
|
+
else
|
70
|
+
vals.map(&:first)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
data/config.ru
ADDED
data/datascope.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "datascope"
|
7
|
+
gem.version = "0.0.1"
|
8
|
+
gem.authors = ["Will Leinweber"]
|
9
|
+
gem.email = []
|
10
|
+
gem.description = %q{postgres 9.2 visibility}
|
11
|
+
gem.summary = %q{postgres 9.2 visibility}
|
12
|
+
gem.homepage = "https://github.com/will/datascope"
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
|
19
|
+
##
|
20
|
+
# Runtime dependencies.
|
21
|
+
|
22
|
+
gem.add_runtime_dependency "sinatra"
|
23
|
+
gem.add_runtime_dependency "puma"
|
24
|
+
gem.add_runtime_dependency "pg"
|
25
|
+
gem.add_runtime_dependency "sequel"
|
26
|
+
gem.add_runtime_dependency "sequel_pg"
|
27
|
+
gem.add_runtime_dependency "haml"
|
28
|
+
gem.add_runtime_dependency "rack-coffee"
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
context = cubism.context()
|
2
|
+
.step(1e4)
|
3
|
+
.size(960)
|
4
|
+
|
5
|
+
getThing = (name, selector) ->
|
6
|
+
context.metric( (start,stop,step,callback)->
|
7
|
+
url = "/metric?selector=#{selector}&start=#{start.toISOString()}&stop=#{stop.toISOString()}&step=#{step}"
|
8
|
+
d3.json url, (data) ->
|
9
|
+
return callback(new Error('could not load data')) unless data
|
10
|
+
callback(null, data)
|
11
|
+
|
12
|
+
, name)
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
$.getJSON('/queries', (data) ->
|
17
|
+
console.log(data)
|
18
|
+
$ -> _.each(data, (it) -> $('#queries').append("<li><em>#{it.calls}c, #{it.total_time}ms</em> #{it.query}</li>"))
|
19
|
+
)
|
20
|
+
|
21
|
+
$ ->
|
22
|
+
d3.select("#cubism").selectAll(".axis")
|
23
|
+
.data(["top", "bottom"])
|
24
|
+
.enter().append("div")
|
25
|
+
.attr("class", (d)-> return d + " axis" )
|
26
|
+
.each((d) -> d3.select(this).call(context.axis().ticks(12).orient(d)))
|
27
|
+
|
28
|
+
d3.select("#cubism").append("div")
|
29
|
+
.attr("class", "rule")
|
30
|
+
.call(context.rule())
|
31
|
+
|
32
|
+
d3.select("#cubism").selectAll(".horizon")
|
33
|
+
.data([
|
34
|
+
getThing('conn count' , 'connections') ,
|
35
|
+
getThing('cache hit' , 'cache_hit') ,
|
36
|
+
getThing('SELECT (count)' , 'select') ,
|
37
|
+
getThing('SELECT (ms)' , 'select_ms') ,
|
38
|
+
getThing('UPDATE (count)' , 'update') ,
|
39
|
+
getThing('UPDATE (ms)' , 'update_ms') ,
|
40
|
+
getThing('INSERT (count)' , 'insert') ,
|
41
|
+
getThing('INSERT (ms)' , 'insert_ms') ,
|
42
|
+
getThing('locks' , 'locks') ,
|
43
|
+
getThing('voodoo query (ms)', 'query_1') ,
|
44
|
+
])
|
45
|
+
.enter().insert("div", ".bottom")
|
46
|
+
.attr("class", "horizon")
|
47
|
+
.call(context.horizon().height(60)) #.extent([0, 15]))
|
48
|
+
|
49
|
+
context.on("focus", (i) ->
|
50
|
+
d3.selectAll(".value").style("right", i == null ? null : context.size() - i + "px")
|
51
|
+
)
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'json'
|
3
|
+
DB = Sequel.connect ENV['DATABASE_URL']
|
4
|
+
TARGET_DB = Sequel.connect ENV['TARGET_DB']
|
5
|
+
|
6
|
+
module StatCollector
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def stats
|
10
|
+
{
|
11
|
+
connections: connections,
|
12
|
+
stat_statements: stat_statements,
|
13
|
+
cache_hit: cache_hit,
|
14
|
+
locks: locks
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def capture_stats!
|
19
|
+
s = stats
|
20
|
+
DB[:stats] << {data: s.to_json}
|
21
|
+
"connections=#{s[:connections]} stat_statements=#{s[:stat_statements].count} cache_hit=#{cache_hit}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def reset_target_stats!
|
25
|
+
target_db.execute "select pg_stat_statements_reset()"
|
26
|
+
end
|
27
|
+
|
28
|
+
def target_db
|
29
|
+
TARGET_DB
|
30
|
+
end
|
31
|
+
|
32
|
+
def connections
|
33
|
+
target_db[:pg_stat_activity].count
|
34
|
+
end
|
35
|
+
|
36
|
+
def stat_statements
|
37
|
+
TARGET_DB[:pg_stat_statements]
|
38
|
+
.select(:query, :calls, :total_time)
|
39
|
+
.exclude(query: '<insufficient privilege>')
|
40
|
+
.all
|
41
|
+
end
|
42
|
+
|
43
|
+
def cache_hit
|
44
|
+
target_db[:pg_statio_user_tables]
|
45
|
+
.select("(sum(heap_blks_hit) - sum(heap_blks_read)) / sum(heap_blks_hit) as ratio".lit)
|
46
|
+
.first[:ratio]
|
47
|
+
.to_f
|
48
|
+
end
|
49
|
+
|
50
|
+
def locks
|
51
|
+
target_db[:pg_locks].exclude(:granted).count
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
body {
|
2
|
+
font-family: "Helvetica Neue", Helvetica, sans-serif;
|
3
|
+
margin: 30px auto;
|
4
|
+
width: 960px;
|
5
|
+
position: relative;
|
6
|
+
}
|
7
|
+
|
8
|
+
header {
|
9
|
+
padding: 6px 0;
|
10
|
+
}
|
11
|
+
|
12
|
+
.group {
|
13
|
+
margin-bottom: 1em;
|
14
|
+
}
|
15
|
+
|
16
|
+
.axis {
|
17
|
+
font: 10px sans-serif;
|
18
|
+
position: fixed;
|
19
|
+
pointer-events: none;
|
20
|
+
z-index: 2;
|
21
|
+
}
|
22
|
+
|
23
|
+
.axis text {
|
24
|
+
-webkit-transition: fill-opacity 250ms linear;
|
25
|
+
}
|
26
|
+
|
27
|
+
.axis path {
|
28
|
+
display: none;
|
29
|
+
}
|
30
|
+
|
31
|
+
.axis line {
|
32
|
+
stroke: #000;
|
33
|
+
shape-rendering: crispEdges;
|
34
|
+
}
|
35
|
+
|
36
|
+
.axis.top {
|
37
|
+
background-image: linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
|
38
|
+
background-image: -o-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
|
39
|
+
background-image: -moz-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
|
40
|
+
background-image: -webkit-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
|
41
|
+
background-image: -ms-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
|
42
|
+
top: 0px;
|
43
|
+
padding: 0 0 24px 0;
|
44
|
+
}
|
45
|
+
|
46
|
+
.axis.bottom {
|
47
|
+
background-image: linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
|
48
|
+
background-image: -o-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
|
49
|
+
background-image: -moz-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
|
50
|
+
background-image: -webkit-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
|
51
|
+
background-image: -ms-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
|
52
|
+
bottom: 0px;
|
53
|
+
padding: 24px 0 0 0;
|
54
|
+
}
|
55
|
+
|
56
|
+
.horizon {
|
57
|
+
border-bottom: solid 1px #000;
|
58
|
+
overflow: hidden;
|
59
|
+
position: relative;
|
60
|
+
}
|
61
|
+
|
62
|
+
.horizon {
|
63
|
+
border-top: solid 1px #000;
|
64
|
+
border-bottom: solid 1px #000;
|
65
|
+
}
|
66
|
+
|
67
|
+
.horizon + .horizon {
|
68
|
+
border-top: none;
|
69
|
+
}
|
70
|
+
|
71
|
+
.horizon canvas {
|
72
|
+
display: block;
|
73
|
+
}
|
74
|
+
|
75
|
+
.horizon .title,
|
76
|
+
.horizon .value {
|
77
|
+
bottom: 0;
|
78
|
+
line-height: 30px;
|
79
|
+
margin: 0 6px;
|
80
|
+
position: absolute;
|
81
|
+
text-shadow: 0 1px 0 rgba(255,255,255,.5);
|
82
|
+
white-space: nowrap;
|
83
|
+
}
|
84
|
+
|
85
|
+
.horizon .title {
|
86
|
+
left: 0;
|
87
|
+
}
|
88
|
+
|
89
|
+
.horizon .value {
|
90
|
+
right: 0;
|
91
|
+
}
|
92
|
+
|
93
|
+
.line {
|
94
|
+
background: #000;
|
95
|
+
opacity: .2;
|
96
|
+
z-index: 2;
|
97
|
+
}
|
98
|
+
|
99
|
+
@media all and (max-width: 1439px) {
|
100
|
+
body { margin: 0px auto; }
|
101
|
+
.axis { position: static; }
|
102
|
+
.axis.top, .axis.bottom { padding: 0; }
|
103
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
// Backbone.js 0.9.2
|
2
|
+
|
3
|
+
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
4
|
+
// Backbone may be freely distributed under the MIT license.
|
5
|
+
// For all details and documentation:
|
6
|
+
// http://backbonejs.org
|
7
|
+
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
|
8
|
+
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
|
9
|
+
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
|
10
|
+
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
|
11
|
+
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
|
12
|
+
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
|
13
|
+
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
|
14
|
+
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
|
15
|
+
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
|
16
|
+
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
|
17
|
+
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
|
18
|
+
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
|
19
|
+
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
|
20
|
+
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
|
21
|
+
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
|
22
|
+
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
|
23
|
+
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
|
24
|
+
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
|
25
|
+
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
|
26
|
+
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
|
27
|
+
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
|
28
|
+
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
|
29
|
+
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
|
30
|
+
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
|
31
|
+
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
|
32
|
+
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
|
33
|
+
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
|
34
|
+
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
|
35
|
+
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
|
36
|
+
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
|
37
|
+
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
|
38
|
+
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
|