graffable 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 +7 -0
- data/CHANGES.md +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +89 -0
- data/Rakefile +11 -0
- data/TODO.md +9 -0
- data/config.ru +8 -0
- data/db/migrations/001_create_groups.rb +14 -0
- data/db/migrations/002_create_reports.rb +17 -0
- data/db/migrations/003_create_numbers.rb +25 -0
- data/graffable.gemspec +31 -0
- data/lib/graffable.rb +7 -0
- data/lib/graffable/app.rb +231 -0
- data/lib/graffable/database.rb +14 -0
- data/lib/graffable/importer.rb +63 -0
- data/lib/graffable/migration_task.rb +52 -0
- data/lib/graffable/public/favicon.ico +0 -0
- data/lib/graffable/public/javascript/jquery.flot.tooltip.min-0.6.5.js +12 -0
- data/lib/graffable/public/stylesheets/graffable.css +8 -0
- data/lib/graffable/seed_task.rb +26 -0
- data/lib/graffable/version.rb +4 -0
- data/lib/graffable/views/group.haml +6 -0
- data/lib/graffable/views/index.haml +6 -0
- data/lib/graffable/views/layout.haml +64 -0
- data/lib/graffable/views/report.haml +63 -0
- metadata +182 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e68f6c232ecbb7293cc71a1abc7435e98b6e3b0f
|
4
|
+
data.tar.gz: d2059c65cfa780aec4164843ecc664d046cd9c69
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 54b73fd518fe5249903dd32b7dcd6c5ed47b645d3ad9a45b0b9002e12ffd964667a3e88e17b750a7c789fe4ed4a3e52cc0ff707ce5e249c3401cc57e31d28777
|
7
|
+
data.tar.gz: 9d1c0e3058e8c2d35e62b4b882f6ea06004557d88bb39682281b326b84a3721018d250bff26036678e85330947f13f727e64cbcf1915dd7cdca9a680192cf8d8
|
data/.gitignore
ADDED
data/CHANGES.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 blair christensen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Graffable
|
2
|
+
|
3
|
+
Sinatra-based data charting application
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'graffable'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install graffable
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Set environment variables:
|
22
|
+
|
23
|
+
% export GRAFFABLE_DATABASE_URL=sqlite://path/to/db.sqlite3
|
24
|
+
% export GRAFFABLE_SEED_FILE=/path/to/seed.rb
|
25
|
+
|
26
|
+
Create seed file:
|
27
|
+
|
28
|
+
require 'graffable'
|
29
|
+
|
30
|
+
DB = Graffable::Database.connect
|
31
|
+
|
32
|
+
# Report Groups
|
33
|
+
[
|
34
|
+
[ 'a', 'Service A' ],
|
35
|
+
[ 'b', 'Host B', ]
|
36
|
+
].each do |tuple|
|
37
|
+
DB[:groups].insert name: tuple.first, description: tuple.last
|
38
|
+
end
|
39
|
+
|
40
|
+
# Reports
|
41
|
+
{
|
42
|
+
'a' => {
|
43
|
+
'report-a1' => [ 'Name of report', :sum ],
|
44
|
+
'report-a2' => [ 'Name of report', :avg ],
|
45
|
+
},
|
46
|
+
'b' => {
|
47
|
+
'report-b1' => [ 'Name of report', :sum ],
|
48
|
+
'report-b2' => [ 'Name of report', :avg ],
|
49
|
+
}
|
50
|
+
}.each_pair do |group_name, reports|
|
51
|
+
group = DB[:groups][ name: group_name ]
|
52
|
+
raise "ERROR: unknown report group '#{ group_name }'" if group.nil?
|
53
|
+
|
54
|
+
reports.each_pair do |name, values|
|
55
|
+
DB[:reports].insert group_id: group[:id], name: name, description: values.first, aggregate: values.last.to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
Add tasks to `Rakefile`:
|
61
|
+
|
62
|
+
% cat Rakefile
|
63
|
+
...
|
64
|
+
require 'graffable/migration_task'
|
65
|
+
Graffable::MigrationTask.new
|
66
|
+
|
67
|
+
require 'graffable/seed_task'
|
68
|
+
Graffable::SeedTask.new
|
69
|
+
...
|
70
|
+
|
71
|
+
|
72
|
+
Migrate database and load seed data:
|
73
|
+
|
74
|
+
% rake graffable:migrate:reset graffable:seed
|
75
|
+
|
76
|
+
Create `config.ru` and launch application:
|
77
|
+
|
78
|
+
% cat config.ru
|
79
|
+
require 'graffable'
|
80
|
+
run Graffable::App
|
81
|
+
% rackup
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
|
85
|
+
1. Fork it
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
require './lib/graffable.rb'
|
4
|
+
ENV['GRAFFABLE_DATABASE_URL'] ||= 'sqlite://db/development.sqlite3'
|
5
|
+
|
6
|
+
require './lib/graffable/migration_task.rb'
|
7
|
+
Graffable::MigrationTask.new
|
8
|
+
|
9
|
+
require './lib/graffable/seed_task.rb'
|
10
|
+
Graffable::SeedTask.new
|
11
|
+
|
data/TODO.md
ADDED
data/config.ru
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
Sequel.migration do
|
3
|
+
transaction
|
4
|
+
change do
|
5
|
+
create_table(:reports) do
|
6
|
+
primary_key :id
|
7
|
+
foreign_key :group_id, :groups
|
8
|
+
String :name, null: false
|
9
|
+
String :description
|
10
|
+
String :aggregate, null: false
|
11
|
+
index :group_id
|
12
|
+
index :name
|
13
|
+
unique [ :group_id, :name ]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
Sequel.migration do
|
3
|
+
transaction
|
4
|
+
|
5
|
+
change do
|
6
|
+
create_table(:numbers) do
|
7
|
+
primary_key :id
|
8
|
+
foreign_key :report_id, :reports
|
9
|
+
String :value, null: false
|
10
|
+
String :year, size: 4
|
11
|
+
String :month, size: 2
|
12
|
+
String :day, size: 2
|
13
|
+
String :hour, size: 2
|
14
|
+
String :label
|
15
|
+
index :report_id
|
16
|
+
index :year
|
17
|
+
index :month
|
18
|
+
index :day
|
19
|
+
index :hour
|
20
|
+
unique [ :report_id, :year, :month, :day, :hour, :label ]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
data/graffable.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'graffable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'graffable'
|
8
|
+
spec.version = Graffable::VERSION
|
9
|
+
spec.authors = ["blair christensen."]
|
10
|
+
spec.email = ["blair.christensen@gmail.com"]
|
11
|
+
spec.description = %q{Sinatra-based data charting application}
|
12
|
+
spec.summary = %q{Sinatra-based data charting application}
|
13
|
+
spec.homepage = 'https://github.com/blairc/graffable'
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'haml'
|
22
|
+
spec.add_dependency 'json'
|
23
|
+
spec.add_dependency 'sequel'
|
24
|
+
spec.add_dependency 'sinatra'
|
25
|
+
spec.add_dependency 'sinatra-flash'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
28
|
+
spec.add_development_dependency 'rake'
|
29
|
+
spec.add_development_dependency 'sqlite3'
|
30
|
+
end
|
31
|
+
|
data/lib/graffable.rb
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'date'
|
3
|
+
require 'haml'
|
4
|
+
require 'json'
|
5
|
+
require 'sinatra/base'
|
6
|
+
require 'sinatra/flash'
|
7
|
+
|
8
|
+
|
9
|
+
module Graffable
|
10
|
+
|
11
|
+
class App < Sinatra::Base
|
12
|
+
|
13
|
+
configure do
|
14
|
+
set :root, File.dirname(__FILE__)
|
15
|
+
set :haml, { format: :html5 }
|
16
|
+
set :static, true
|
17
|
+
enable :sessions
|
18
|
+
|
19
|
+
register Sinatra::Flash
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
before do
|
24
|
+
@db = Graffable::Database.connect # Sequel.connect settings.database_url
|
25
|
+
@groups = @db[:groups].order(:name)
|
26
|
+
end
|
27
|
+
|
28
|
+
# /:group/:report/:year/:month.json
|
29
|
+
get %r{/(.+)/(.+)/(\d{4})/(\d{2}).(csv|json)} do |group_name, report_name, year, month, extension|
|
30
|
+
group = assert_group group_name
|
31
|
+
report = assert_report group, report_name
|
32
|
+
|
33
|
+
# TODO DRY?
|
34
|
+
query = "SELECT year,month,day,label,#{ report[:aggregate].to_s.upcase }(value) AS value " +
|
35
|
+
"FROM numbers " +
|
36
|
+
"WHERE report_id=:report_id " +
|
37
|
+
"AND year=:year " +
|
38
|
+
"AND month=:month " +
|
39
|
+
"GROUP BY year,month,day,label " +
|
40
|
+
"ORDER BY year,month,day,label"
|
41
|
+
dataset = @db.fetch( query, month: month, report_id: report[:id], year: year )
|
42
|
+
|
43
|
+
case extension
|
44
|
+
when 'csv'
|
45
|
+
return_csv dataset
|
46
|
+
when 'json'
|
47
|
+
return_json dataset, date_formatter: lambda { |y,m,d,h| d }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# /:group/:report/:year.json
|
52
|
+
get %r{/(.+)/(.+)/(\d{4}).(csv|json)} do |group_name, report_name, year, extension|
|
53
|
+
group = assert_group group_name
|
54
|
+
report = assert_report group, report_name
|
55
|
+
|
56
|
+
# TODO DRY?
|
57
|
+
query = "SELECT year,month,label,#{ report[:aggregate].to_s.upcase }(value) AS value " +
|
58
|
+
"FROM numbers " +
|
59
|
+
"WHERE report_id=:report_id " +
|
60
|
+
"AND year=:year " +
|
61
|
+
"GROUP BY year,month,label " +
|
62
|
+
"ORDER BY year,month,label"
|
63
|
+
dataset = @db.fetch( query, report_id: report[:id], year: year )
|
64
|
+
|
65
|
+
case extension
|
66
|
+
when 'csv'
|
67
|
+
return_csv dataset
|
68
|
+
when 'json'
|
69
|
+
return_json dataset, date_formatter: lambda { |y,m,d,h| "#{y}-#{m}" }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# /:group/:report.json
|
74
|
+
get %r{/(.+)/(.+).(csv|json)} do |group_name, report_name, extension|
|
75
|
+
group = assert_group group_name
|
76
|
+
report = assert_report group, report_name
|
77
|
+
|
78
|
+
# TODO DRY?
|
79
|
+
query = "SELECT year,month,day,label,#{ report[:aggregate].to_s.upcase }(value) AS value " +
|
80
|
+
"FROM numbers " +
|
81
|
+
"WHERE report_id=:report_id " +
|
82
|
+
"GROUP BY year,month,day,label " +
|
83
|
+
"ORDER BY year,month,day,label"
|
84
|
+
dataset = @db.fetch( query, report_id: report[:id] )
|
85
|
+
max = 15
|
86
|
+
|
87
|
+
case extension
|
88
|
+
when 'csv'
|
89
|
+
return_csv dataset, max: max
|
90
|
+
when 'json'
|
91
|
+
return_json dataset, date_formatter: lambda { |y,m,d,h| Date.parse("#{y}-#{m}-#{d}").strftime("%b %d") }, max: max
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# TODO DRY
|
96
|
+
get %r{/(.+)/(.+)/(\d{4})/(\d{2})} do |group_name, report_name, year, month|
|
97
|
+
@group = @groups.detect { |g| group_name == g[:name] }
|
98
|
+
redirect_with_warning( '/', "Report group not found: #{group_name}" ) unless @group
|
99
|
+
|
100
|
+
@reports = @db[:reports].where( group_id: @group[:id] ).all
|
101
|
+
@report = @reports.detect { |r| report_name == r[:name] }
|
102
|
+
redirect_with_warning( "/#{group_name}", "Report not found: #{report_name}" ) unless @report
|
103
|
+
|
104
|
+
@data_url = "/#{group_name}/#{report_name}/#{year}/#{month}.json"
|
105
|
+
@interval = "#{year}-#{month}"
|
106
|
+
|
107
|
+
date = Date.parse( "#{year}-#{month}-01" )
|
108
|
+
@previous = "/#{group_name}/#{report_name}/#{ date.prev_month.year }/#{ date.prev_month.strftime('%m') }"
|
109
|
+
@next = "/#{group_name}/#{report_name}/#{ date.next_month.year }/#{ date.next_month.strftime('%m') }"
|
110
|
+
|
111
|
+
haml :report
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO DRY
|
115
|
+
get %r{/(.+)/(.+)/(\d{4})} do |group_name, report_name, year|
|
116
|
+
@group = @groups.detect { |g| group_name == g[:name] }
|
117
|
+
redirect_with_warning( '/', "Report group not found: #{group_name}" ) unless @group
|
118
|
+
|
119
|
+
@reports = @db[:reports].where( group_id: @group[:id] ).all
|
120
|
+
@report = @reports.detect { |r| report_name == r[:name] }
|
121
|
+
redirect_with_warning( "/#{group_name}", "Report not found: #{report_name}" ) unless @report
|
122
|
+
|
123
|
+
@data_url = "/#{group_name}/#{report_name}/#{year}.json"
|
124
|
+
@interval = year
|
125
|
+
|
126
|
+
date = Date.parse( "#{year}-01-01" )
|
127
|
+
@previous = "/#{group_name}/#{report_name}/#{ date.prev_year.year }"
|
128
|
+
@next = "/#{group_name}/#{report_name}/#{ date.next_year.year }"
|
129
|
+
|
130
|
+
haml :report
|
131
|
+
end
|
132
|
+
|
133
|
+
# TODO DRY
|
134
|
+
get '/:group/:report' do
|
135
|
+
@group = @groups.detect { |g| params[:group] == g[:name] }
|
136
|
+
redirect_with_warning( '/', "Report group not found: #{ params[:group] }" ) unless @group
|
137
|
+
|
138
|
+
@reports = @db[:reports].where( group_id: @group[:id] ).all
|
139
|
+
@report = @reports.detect { |r| params[:report] == r[:name] }
|
140
|
+
redirect_with_warning( "/#{ params[:group] }", "Report not found: #{ params[:report] }" ) unless @report
|
141
|
+
|
142
|
+
@data_url = "/#{ params[:group] }/#{ params[:report] }.json"
|
143
|
+
@interval = 'Recent'
|
144
|
+
|
145
|
+
haml :report
|
146
|
+
end
|
147
|
+
|
148
|
+
get '/:group' do
|
149
|
+
@group = @groups.detect{ |g| params[:group] == g[:name] }
|
150
|
+
redirect_with_warning( '/', "Report group not found: #{ params[:group] }" ) unless @group
|
151
|
+
|
152
|
+
@reports = @db[:reports].where( group_id: @group[:id] ).all
|
153
|
+
haml :group
|
154
|
+
end
|
155
|
+
|
156
|
+
get '/' do
|
157
|
+
haml :index
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def assert_group(group_name)
|
164
|
+
group = @groups.detect { |g| group_name == g[:name] }
|
165
|
+
halt 404, "Report group not found: #{group_name}" unless group
|
166
|
+
group
|
167
|
+
end
|
168
|
+
|
169
|
+
def assert_report(group, report_name)
|
170
|
+
reports = @db[:reports].where( group_id: group[:id] ).all
|
171
|
+
report = reports.detect { |r| report_name == r[:name] }
|
172
|
+
halt 404, "Report not found: #{report_name}" unless report
|
173
|
+
report
|
174
|
+
end
|
175
|
+
|
176
|
+
def redirect_with_warning(path, message)
|
177
|
+
flash[:warning] = message
|
178
|
+
redirect to(path)
|
179
|
+
end
|
180
|
+
|
181
|
+
def return_csv( dataset, params = {} )
|
182
|
+
defaults = { max: -1 }
|
183
|
+
opts = defaults.merge params
|
184
|
+
|
185
|
+
data = [ %w( date label value ).to_csv ]
|
186
|
+
dataset.each do |row|
|
187
|
+
date = %i( year month day hour ).collect { |k| row[k] }.compact.join('-')
|
188
|
+
label = row[:label] || ''
|
189
|
+
value = row[:value].to_i
|
190
|
+
data << [ date, label, value ].to_csv
|
191
|
+
end
|
192
|
+
|
193
|
+
if opts[:max] > -1
|
194
|
+
headers = data.shift
|
195
|
+
data = [ headers, data.reverse.slice( 0 .. ( opts[:max] - 1 ) ).reverse ].flatten
|
196
|
+
end
|
197
|
+
|
198
|
+
content_type :text
|
199
|
+
return data.join
|
200
|
+
end
|
201
|
+
|
202
|
+
def return_json( dataset, params = {} )
|
203
|
+
defaults = { max: -1 }
|
204
|
+
opts = defaults.merge params
|
205
|
+
|
206
|
+
data = {}
|
207
|
+
dataset.each do |row|
|
208
|
+
date = opts[:date_formatter].call row[:year], row[:month], row[:day], row[:hour]
|
209
|
+
label = row[:label] || ''
|
210
|
+
value = row[:value].to_i
|
211
|
+
|
212
|
+
unless data.key?(label)
|
213
|
+
data[label] = { data: [] }
|
214
|
+
data[label][:label] = label unless label.empty?
|
215
|
+
end
|
216
|
+
data[label][:data].push [ date, value ]
|
217
|
+
end
|
218
|
+
|
219
|
+
if opts[:max] > -1
|
220
|
+
data.each_pair do |label, values|
|
221
|
+
data[label][:data] = data[label][:data].reverse.slice( 0 .. ( opts[:max] - 1 ) ).reverse
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
content_type :json
|
226
|
+
return { data: data.values }.to_json
|
227
|
+
end
|
228
|
+
|
229
|
+
end # class Graffable::App
|
230
|
+
end # module Graffable
|
231
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module Graffable
|
4
|
+
|
5
|
+
class Database
|
6
|
+
def self.connect
|
7
|
+
key = 'GRAFFABLE_DATABASE_URL'
|
8
|
+
raise "ERROR: #{key} not defined" unless ENV.key?(key)
|
9
|
+
Sequel.connect ENV[key]
|
10
|
+
end
|
11
|
+
|
12
|
+
end # class Graffable::Database
|
13
|
+
end # module Graffable
|
14
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
require 'graffable'
|
3
|
+
|
4
|
+
module Graffable
|
5
|
+
class Importer
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@db = Graffable::Database.connect
|
9
|
+
@groups = {}
|
10
|
+
@reports = {}
|
11
|
+
yield self if block_given?
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def transaction(&block)
|
16
|
+
@db.transaction block
|
17
|
+
end
|
18
|
+
|
19
|
+
def insert( group_name, report_name, value, params = {} )
|
20
|
+
defaults = {
|
21
|
+
day: nil, hour: nil, label: nil, month: nil, year: nil
|
22
|
+
}
|
23
|
+
opts = defaults.merge(params)
|
24
|
+
r = _find_report group_name, report_name
|
25
|
+
|
26
|
+
# TODO Silently skip existing rows? And why isn't the database enforcing this?
|
27
|
+
return if @db[:numbers][ report_id: r[:id], year: opts[:year], month: opts[:month], day: opts[:day], hour: opts[:hour], label: opts[:label] ]
|
28
|
+
|
29
|
+
@db.transaction do
|
30
|
+
rv = @db[:numbers].insert report_id: r[:id],
|
31
|
+
value: value,
|
32
|
+
year: opts[:year],
|
33
|
+
month: opts[:month],
|
34
|
+
day: opts[:day],
|
35
|
+
hour: opts[:hour],
|
36
|
+
label: opts[:label]
|
37
|
+
raise "insert failed - #{rv.inspect}" unless rv > 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def _find_report(group, report)
|
45
|
+
unless @groups.key?(group)
|
46
|
+
@groups[group] = @db[:groups][ name: group ]
|
47
|
+
end
|
48
|
+
g = @groups[group]
|
49
|
+
raise "invalid group - #{group}" unless g
|
50
|
+
|
51
|
+
unless @reports.key?(group)
|
52
|
+
@reports[group] = {}
|
53
|
+
@db[:reports].where( group_id: g[:id] ).each { |row| @reports[group][ row[:name] ] = row }
|
54
|
+
end
|
55
|
+
r = @reports[group][report]
|
56
|
+
raise "invalid report - #{report}" unless r
|
57
|
+
|
58
|
+
r
|
59
|
+
end
|
60
|
+
|
61
|
+
end # class Graffable::Importer
|
62
|
+
end # module Graffable
|
63
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'graffable'
|
4
|
+
|
5
|
+
module Graffable
|
6
|
+
class MigrationTask < ::Rake::TaskLib
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
|
10
|
+
Sequel.extension :migration
|
11
|
+
db = Graffable::Database.connect
|
12
|
+
namespace = 'graffable:migrate'
|
13
|
+
migrations = File.join( File.dirname(__FILE__), '../../db/migrations' )
|
14
|
+
|
15
|
+
desc 'Perform migration down (erase all data)'
|
16
|
+
task "#{namespace}:down" do
|
17
|
+
Sequel::Migrator.run db, migrations, target: 0
|
18
|
+
puts "<= #{namespace}:down executed"
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Perform migration reset (full erase and migration up)'
|
22
|
+
task "#{namespace}:reset" do
|
23
|
+
Sequel::Migrator.run db, migrations, target: 0
|
24
|
+
Sequel::Migrator.run db, migrations
|
25
|
+
puts "<= #{namespace}:reset executed"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'Perform migration down (erase all data)'
|
29
|
+
task "#{namespace}:down" do
|
30
|
+
Sequel::Migrator.run db, migrations, target: 0
|
31
|
+
puts "<= #{namespace}:down executed"
|
32
|
+
end
|
33
|
+
|
34
|
+
desc 'Perform migration up/down to VERSION'
|
35
|
+
task "#{namespace}:to" do
|
36
|
+
version = ENV['VERSION'].to_i
|
37
|
+
raise 'No VERSION was provided' if version.nil?
|
38
|
+
Sequel::Migrator.run db, migrations, target: version
|
39
|
+
puts "<= #{namespace}:to version=[#{version}] executed"
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'Perform migration to latest migration available'
|
43
|
+
task "#{namespace}:up" do
|
44
|
+
Sequel::Migrator.run db, migrations
|
45
|
+
puts "<= #{namespace}:up executed"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end # class Graffable::MigrationTask
|
51
|
+
end # module Graffable
|
52
|
+
|
Binary file
|
@@ -0,0 +1,12 @@
|
|
1
|
+
/*
|
2
|
+
* jquery.flot.tooltip
|
3
|
+
*
|
4
|
+
* description: easy-to-use tooltips for Flot charts
|
5
|
+
* version: 0.6.5
|
6
|
+
* author: Krzysztof Urbas @krzysu [myviews.pl]
|
7
|
+
* website: https://github.com/krzysu/flot.tooltip
|
8
|
+
*
|
9
|
+
* build on 2014-01-23
|
10
|
+
* released under MIT License, 2012
|
11
|
+
*/
|
12
|
+
(function(t){var i={tooltip:!1,tooltipOpts:{content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,monthNames:null,dayNames:null,shifts:{x:10,y:20},defaultTheme:!0,onHover:function(){}}},o=function(t){this.tipPosition={x:0,y:0},this.init(t)};o.prototype.init=function(i){function o(t){var i={};i.x=t.pageX,i.y=t.pageY,s.updateTooltipPosition(i)}function e(t,i,o){var e=s.getDomElement();if(o){var n;n=s.stringFormat(s.tooltipOptions.content,o),e.html(n),s.updateTooltipPosition({x:i.pageX,y:i.pageY}),e.css({left:s.tipPosition.x+s.tooltipOptions.shifts.x,top:s.tipPosition.y+s.tooltipOptions.shifts.y}).show(),"function"==typeof s.tooltipOptions.onHover&&s.tooltipOptions.onHover(o,e)}else e.hide().html("")}var s=this;i.hooks.bindEvents.push(function(i,n){s.plotOptions=i.getOptions(),s.plotOptions.tooltip!==!1&&void 0!==s.plotOptions.tooltip&&(s.tooltipOptions=s.plotOptions.tooltipOpts,s.getDomElement(),t(i.getPlaceholder()).bind("plothover",e),t(n).bind("mousemove",o))}),i.hooks.shutdown.push(function(i,s){t(i.getPlaceholder()).unbind("plothover",e),t(s).unbind("mousemove",o)})},o.prototype.getDomElement=function(){var i;return t("#flotTip").length>0?i=t("#flotTip"):(i=t("<div />").attr("id","flotTip"),i.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&i.css({background:"#fff","z-index":"100",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111",display:"none","white-space":"nowrap"})),i},o.prototype.updateTooltipPosition=function(i){var o=t("#flotTip").outerWidth()+this.tooltipOptions.shifts.x,e=t("#flotTip").outerHeight()+this.tooltipOptions.shifts.y;i.x-t(window).scrollLeft()>t(window).innerWidth()-o&&(i.x-=o),i.y-t(window).scrollTop()>t(window).innerHeight()-e&&(i.y-=e),this.tipPosition.x=i.x,this.tipPosition.y=i.y},o.prototype.stringFormat=function(t,i){var o,e,s=/%p\.{0,1}(\d{0,})/,n=/%s/,p=/%x\.{0,1}(\d{0,})/,a=/%y\.{0,1}(\d{0,})/,r="%x",l="%y";return i.series.threshold!==void 0?(o=i.datapoint[0],e=i.datapoint[1]):(o=i.series.data[i.dataIndex][0],e=i.series.data[i.dataIndex][1]),null===i.series.label&&i.series.originSeries&&(i.series.label=i.series.originSeries.label),"function"==typeof t&&(t=t(i.series.label,o,e,i)),i.series.percent!==void 0&&(t=this.adjustValPrecision(s,t,i.series.percent)),t=i.series.label!==void 0?t.replace(n,i.series.label):t.replace(n,""),this.isTimeMode("xaxis",i)&&this.isXDateFormat(i)&&(t=t.replace(p,this.timestampToDate(o,this.tooltipOptions.xDateFormat))),this.isTimeMode("yaxis",i)&&this.isYDateFormat(i)&&(t=t.replace(a,this.timestampToDate(e,this.tooltipOptions.yDateFormat))),"number"==typeof o&&(t=this.adjustValPrecision(p,t,o)),"number"==typeof e&&(t=this.adjustValPrecision(a,t,e)),i.series.xaxis.ticks!==void 0&&i.series.xaxis.ticks.length>i.dataIndex&&!this.isTimeMode("xaxis",i)&&(t=t.replace(p,i.series.xaxis.ticks[i.dataIndex].label)),i.series.xaxis.tickFormatter!==void 0&&(t=t.replace(r,i.series.xaxis.tickFormatter(o,i.series.xaxis).replace(/\$/g,"$$"))),i.series.yaxis.tickFormatter!==void 0&&(t=t.replace(l,i.series.yaxis.tickFormatter(e,i.series.yaxis).replace(/\$/g,"$$"))),t},o.prototype.isTimeMode=function(t,i){return i.series[t].options.mode!==void 0&&"time"===i.series[t].options.mode},o.prototype.isXDateFormat=function(){return this.tooltipOptions.xDateFormat!==void 0&&null!==this.tooltipOptions.xDateFormat},o.prototype.isYDateFormat=function(){return this.tooltipOptions.yDateFormat!==void 0&&null!==this.tooltipOptions.yDateFormat},o.prototype.timestampToDate=function(i,o){var e=new Date(1*i);return t.plot.formatDate(e,o,this.tooltipOptions.monthNames,this.tooltipOptions.dayNames)},o.prototype.adjustValPrecision=function(t,i,o){var e,s=i.match(t);return null!==s&&""!==RegExp.$1&&(e=RegExp.$1,o=o.toFixed(e),i=i.replace(t,o)),i};var e=function(t){new o(t)};t.plot.plugins.push({init:e,options:i,name:"tooltip",version:"0.6.1"})})(jQuery);
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'graffable'
|
4
|
+
|
5
|
+
module Graffable
|
6
|
+
class SeedTask < ::Rake::TaskLib
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
|
10
|
+
db = Graffable::Database.connect
|
11
|
+
namespace = 'graffable'
|
12
|
+
|
13
|
+
desc 'Load Graffable seed data from GRAFFABLE_SEED_FILE'
|
14
|
+
task "#{namespace}:seed" do
|
15
|
+
key = 'GRAFFABLE_SEED_FILE'
|
16
|
+
seed = ENV[key]
|
17
|
+
raise "No #{key} was provided" if seed.nil?
|
18
|
+
load seed
|
19
|
+
puts "<= #{namespace}:seed executed"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end # class Graffable::SeedTask
|
25
|
+
end # module Graffable
|
26
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
%html
|
2
|
+
%head
|
3
|
+
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }
|
4
|
+
%link{ rel: 'stylesheet', type: 'text/css', href: '//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css' }
|
5
|
+
%link{ rel: 'stylesheet', type: 'text/css', href: url('/stylesheets/graffable.css') }
|
6
|
+
%title= 'Metrics'
|
7
|
+
%body
|
8
|
+
%script{ type: 'text/javascript', src: '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js' }
|
9
|
+
%script{ type: 'text/javascript', src: '//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js' }
|
10
|
+
%script{ type: 'text/javascript', src: '//cdnjs.cloudflare.com/ajax/libs/flot/0.8.2/jquery.flot.min.js' }
|
11
|
+
%script{ type: 'text/javascript', src: '//cdnjs.cloudflare.com/ajax/libs/flot/0.8.2/jquery.flot.categories.min.js' }
|
12
|
+
%script{ type: 'text/javascript', src: '//cdnjs.cloudflare.com/ajax/libs/flot/0.8.2/jquery.flot.stack.min.js' }
|
13
|
+
%script{ type: 'text/javascript', src: url('/javascript/jquery.flot.tooltip.min-0.6.5.js') }
|
14
|
+
|
15
|
+
%nav.navbar.navbar-default{ role: 'navigation' }
|
16
|
+
.navbar-header
|
17
|
+
%button.navbar-toggle{ type: 'button', 'data-toggle' => 'collapse', 'data-target' => '#bs-example-navbar-collapse-1' }
|
18
|
+
%span.sr-only{} Toggle navigation
|
19
|
+
%span.icon-bar
|
20
|
+
%span.icon-bar
|
21
|
+
%span.icon-bar
|
22
|
+
%a.navbar-brand{ href: url('/') } Metrics
|
23
|
+
|
24
|
+
.collapse.navbar-collapse{ id: 'bs-example-navbar-collapse-1' }
|
25
|
+
%ul.nav.navbar-nav
|
26
|
+
%li.dropdown
|
27
|
+
%a.dropdown-toggle{ href: '#', 'data-toggle' => 'dropdown' }
|
28
|
+
= @group ? @group[:description] : 'Report Group'
|
29
|
+
%b.caret
|
30
|
+
%ul.dropdown-menu
|
31
|
+
- @groups.each do |g|
|
32
|
+
%li
|
33
|
+
%a{ href: url("/#{ g[:name] }") }= g[:description]
|
34
|
+
- if @group && @reports
|
35
|
+
%li.dropdown
|
36
|
+
%a.dropdown-toggle{ href: '#', 'data-toggle' => 'dropdown' }
|
37
|
+
= @report ? @report[:description] : 'Reports'
|
38
|
+
%b.caret
|
39
|
+
%ul.dropdown-menu
|
40
|
+
- @reports.each do |r|
|
41
|
+
%li
|
42
|
+
%a{ href: url("/#{ @group[:name] }/#{ r[:name] }") }= r[:description]
|
43
|
+
- if @group && @reports && @report
|
44
|
+
%li.dropdown
|
45
|
+
%a.dropdown-toggle{ href: '#', 'data-toggle' => 'dropdown' }
|
46
|
+
= @interval || 'Interval'
|
47
|
+
%b.caret
|
48
|
+
%ul.dropdown-menu
|
49
|
+
-# TODO Helper(s)!
|
50
|
+
- url = "/#{ @group[:name] }/#{ @report[:name] }"
|
51
|
+
- year = Time.now.year
|
52
|
+
- month = Time.now.strftime("%m")
|
53
|
+
%li
|
54
|
+
%a{ href: url(url) } Recent
|
55
|
+
%li
|
56
|
+
%a{ href: url("#{url}/#{year}") }= year
|
57
|
+
%li
|
58
|
+
%a{ href: url("#{url}/#{year}/#{month}") }= "#{year}-#{month}"
|
59
|
+
|
60
|
+
.container
|
61
|
+
.flash
|
62
|
+
=styled_flash
|
63
|
+
= yield
|
64
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
- if @report
|
2
|
+
%div{ style: 'text-align: center' }
|
3
|
+
- if @previous
|
4
|
+
%a{ href: url(@previous) }
|
5
|
+
%span.glyphicon.glyphicon-chevron-left
|
6
|
+
%h1{ style: 'display: inline; padding: 0em 1em;' }= @report[:description]
|
7
|
+
- if @next
|
8
|
+
%a{ href: url(@next) }
|
9
|
+
%span.glyphicon.glyphicon-chevron-right
|
10
|
+
|
11
|
+
:javascript
|
12
|
+
data_url = "#{ url(@data_url) }";
|
13
|
+
|
14
|
+
$(document).ready( function() {
|
15
|
+
|
16
|
+
var options = {
|
17
|
+
bars: {
|
18
|
+
show: true
|
19
|
+
},
|
20
|
+
grid: {
|
21
|
+
hoverable: true
|
22
|
+
},
|
23
|
+
points: {
|
24
|
+
show: true
|
25
|
+
},
|
26
|
+
series: {
|
27
|
+
stack: true
|
28
|
+
},
|
29
|
+
tooltip: true,
|
30
|
+
tooltipOpts: {
|
31
|
+
content: '%s %y'
|
32
|
+
},
|
33
|
+
xaxis: {
|
34
|
+
labelWidth: 3,
|
35
|
+
mode: 'categories',
|
36
|
+
tickDecimals: 0,
|
37
|
+
tickSize: 1
|
38
|
+
},
|
39
|
+
yaxis: {
|
40
|
+
tickDecimals: 0,
|
41
|
+
}
|
42
|
+
};
|
43
|
+
|
44
|
+
var data = [];
|
45
|
+
|
46
|
+
function onDataReceived(series) {
|
47
|
+
// TODO options['yaxis'] = { max: series['max'], min: series['min'], tickDecimals: 0 }
|
48
|
+
$.plot( "#placeholder", series.data, options );
|
49
|
+
}
|
50
|
+
|
51
|
+
$.ajax({
|
52
|
+
url: data_url,
|
53
|
+
type: 'GET',
|
54
|
+
dataType: 'json',
|
55
|
+
success: onDataReceived
|
56
|
+
});
|
57
|
+
|
58
|
+
});
|
59
|
+
|
60
|
+
-# FIXME
|
61
|
+
#content{ style: 'align: center; margin: auto; padding: 1em; width: 95%;' }
|
62
|
+
#placeholder{ style: 'align: center; height: 75%; margin: auto; padding: 1em; width: 95%;' }
|
63
|
+
|
metadata
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graffable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- blair christensen.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: haml
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sequel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sinatra
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sinatra-flash
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.3'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Sinatra-based data charting application
|
126
|
+
email:
|
127
|
+
- blair.christensen@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- CHANGES.md
|
134
|
+
- Gemfile
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- TODO.md
|
139
|
+
- config.ru
|
140
|
+
- db/migrations/001_create_groups.rb
|
141
|
+
- db/migrations/002_create_reports.rb
|
142
|
+
- db/migrations/003_create_numbers.rb
|
143
|
+
- graffable.gemspec
|
144
|
+
- lib/graffable.rb
|
145
|
+
- lib/graffable/app.rb
|
146
|
+
- lib/graffable/database.rb
|
147
|
+
- lib/graffable/importer.rb
|
148
|
+
- lib/graffable/migration_task.rb
|
149
|
+
- lib/graffable/public/favicon.ico
|
150
|
+
- lib/graffable/public/javascript/jquery.flot.tooltip.min-0.6.5.js
|
151
|
+
- lib/graffable/public/stylesheets/graffable.css
|
152
|
+
- lib/graffable/seed_task.rb
|
153
|
+
- lib/graffable/version.rb
|
154
|
+
- lib/graffable/views/group.haml
|
155
|
+
- lib/graffable/views/index.haml
|
156
|
+
- lib/graffable/views/layout.haml
|
157
|
+
- lib/graffable/views/report.haml
|
158
|
+
homepage: https://github.com/blairc/graffable
|
159
|
+
licenses:
|
160
|
+
- MIT
|
161
|
+
metadata: {}
|
162
|
+
post_install_message:
|
163
|
+
rdoc_options: []
|
164
|
+
require_paths:
|
165
|
+
- lib
|
166
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
requirements: []
|
177
|
+
rubyforge_project:
|
178
|
+
rubygems_version: 2.2.0
|
179
|
+
signing_key:
|
180
|
+
specification_version: 4
|
181
|
+
summary: Sinatra-based data charting application
|
182
|
+
test_files: []
|