sequel-reporter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ <div class="page-header">
2
+ <h1>Woops, something went wrong</h1>
3
+ </div>
4
+ <p><%= @error.to_s %></p>
5
+ <p><%= @error.backtrace.join("<br>") %></p>
@@ -0,0 +1,60 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Sequel::Reporter</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="">
8
+ <meta name="author" content="">
9
+
10
+ <link href="/css/bootstrap.css" rel="stylesheet">
11
+ <style>
12
+ body {
13
+ padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
14
+ }
15
+ </style>
16
+
17
+ <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
18
+ <!--[if lt IE 9]>
19
+ <script src="../assets/js/html5shiv.js"></script>
20
+ <![endif]-->
21
+ </head>
22
+
23
+ <body>
24
+
25
+ <div class="navbar navbar-inverse navbar-fixed-top">
26
+ <div class="navbar-inner">
27
+ <div class="container">
28
+ <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
29
+ <span class="icon-bar"></span>
30
+ <span class="icon-bar"></span>
31
+ <span class="icon-bar"></span>
32
+ </button>
33
+ <a class="brand" href="/">Sequel::Reporter</a>
34
+ <div class="nav-collapse collapse">
35
+ <ul class="nav">
36
+ <li class="dropdown">
37
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
38
+ Reports
39
+ <b class="caret"></b>
40
+ </a>
41
+ <ul class="dropdown-menu">
42
+ <% @reports.each do |report| %>
43
+ <li><a href="/reports/<%= report[0] %>"><%= report[1] %></a></li>
44
+ <% end %>
45
+ </ul>
46
+ </li>
47
+ </ul>
48
+ </div><!--/.nav-collapse -->
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="container">
54
+ <%= yield %>
55
+ </div> <!-- /container -->
56
+
57
+ <script src="/js/jquery-1.9.1.js"></script>
58
+ <script src="/js/bootstrap.js"></script>
59
+ </body>
60
+ </html>
@@ -0,0 +1,5 @@
1
+ <h1>Welcome to Sequel::Reporter</h1>
2
+
3
+ <p>
4
+ Replace this file <pre>dashboard.erb</pre> with your first report.
5
+ </p>
@@ -0,0 +1,31 @@
1
+ <% if report.error %>
2
+ <div class="alert-message block-message warning">
3
+ <p><%= report.error %></p>
4
+ <% if report.error.to_s != "No data" %>
5
+ <p><%= report.error.backtrace %></p>
6
+ <% end %>
7
+ </div>
8
+ <% else %>
9
+ <table class="table table-striped table-borderd table-hover table-condensed sorted">
10
+ <thead>
11
+ <tr>
12
+ <% report.fields.each do |f| %>
13
+ <th><span class="<%= f.span_class %>"><%= f.title %></span></th>
14
+ <% end %>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <% report.each_row do |row| %>
19
+ <tr>
20
+ <% row.each do |val| %>
21
+ <td>
22
+ <span class="<%= val[1].span_class %>">
23
+ <%= linkify(links, row, val, (val[1].value_type == 'number' && ! val[0].nil?) ? sprintf("%0.2f", val[0]) : val[0]) %>
24
+ </span>
25
+ </td>
26
+ <% end %>
27
+ </tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
31
+ <% end %>
@@ -0,0 +1,51 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/contrib'
3
+ require 'sequel'
4
+
5
+ module Sequel::Reporter
6
+ class Application < Sinatra::Base
7
+
8
+ helpers Sinatra::Capture
9
+ helpers Sequel::Reporter::Helpers
10
+
11
+ set :render_engine, :erb
12
+ set :database_url, ENV['DATABASE_URL']
13
+
14
+ attr_reader :db
15
+
16
+ before do
17
+ Sequel::Reporter::Report.params = params
18
+
19
+ @reports = []
20
+ Dir.glob(File.join(settings.root, "views", "reports", "*")).each do |report|
21
+ name = File.basename(report, File.extname(report))
22
+ @reports << [name, name.capitalize]
23
+ end
24
+ @reports = @reports.sort {|a,b| a[0] <=> b[0]}
25
+ end
26
+
27
+ get '/' do
28
+ index_report = settings.index_report
29
+ redirect "/reports/#{index_report.to_s}"
30
+ end
31
+
32
+ get '/*' do
33
+ begin
34
+ render settings.render_engine, params[:splat][0].to_sym
35
+ rescue Exception => e
36
+ @error = e
37
+ render :erb, :error
38
+ end
39
+ end
40
+
41
+ def initialize(app=nil)
42
+ @db = Sequel.connect(self.class.settings.database_url)
43
+
44
+ path = File.join(settings.root, "lib", "**", "*.rb")
45
+ Dir.glob(path).each do |file|
46
+ require file
47
+ end
48
+ super(app)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ require 'thor'
2
+
3
+ module Sequel::Reporter
4
+ class CLI < Thor
5
+
6
+ include Thor::Actions
7
+
8
+ desc "new NAME","Create a new Sequel::Reporter project"
9
+ def new(name)
10
+ @name = name
11
+
12
+ empty_directory(name)
13
+ self.destination_root = name
14
+ copy_file("Gemfile")
15
+ template("Rakefile.tt", "Rakefile")
16
+ template("application.rb.tt", "application.rb")
17
+ template("config.ru")
18
+ directory("lib")
19
+ directory("migrate")
20
+ directory("public")
21
+ directory("views")
22
+ end
23
+
24
+ def self.source_root
25
+ File.expand_path("../../../data", __FILE__)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,52 @@
1
+ require 'rack/utils'
2
+ require 'cgi'
3
+
4
+ module Sequel::Reporter
5
+ module Helpers
6
+
7
+ include Rack::Utils
8
+
9
+ def partial(template, locals = {})
10
+ render_engine = locals.delete(:render_engine) || settings.render_engine
11
+ render render_engine, template, :layout => false, :locals => locals
12
+ end
13
+
14
+ def table(report, options = {})
15
+ links = options[:links] || {}
16
+ partial(:table, :report => report, :links => links, :render_engine => :erb)
17
+ end
18
+
19
+ def query(options={}, &block)
20
+ q = capture(&block)
21
+ report = Sequel::Reporter::Report.from_query(@db, q)
22
+ if options[:pivot]
23
+ report = report.pivot(options[:pivot], options[:pivot_sort_order])
24
+ end
25
+ report
26
+ end
27
+
28
+ def linkify(links, row, value, display_value)
29
+ links.each do |key, val|
30
+ if key.is_a? String
31
+ key = /^#{key}$/
32
+ end
33
+
34
+ if key.match(value[1].title.to_s)
35
+ url = String.new(links[key])
36
+ row.each_with_index do |v,i|
37
+ url.gsub!(":#{i}", CGI.escape(v[0].to_s))
38
+ end
39
+
40
+ url.gsub!(':title', CGI.escape(value[1].title.to_s))
41
+ url.gsub!(':now', CGI.escape(DateTime.now.strftime('%Y-%m-%d')))
42
+ display_value = "<a href='#{url}'>#{escape_html(display_value)}</a>"
43
+ else
44
+ display_value = escape_html(display_value)
45
+ end
46
+
47
+ end
48
+ display_value
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,171 @@
1
+ module Sequel::Reporter
2
+ class Field
3
+
4
+ attr_reader :title, :value_type, :span_class
5
+
6
+ def initialize(title, value_type, span_class)
7
+ @title = title
8
+ @value_type = value_type
9
+ @span_class = span_class
10
+ end
11
+
12
+ def ==(other)
13
+ self.title == other.title && \
14
+ self.value_type == other.value_type && \
15
+ self.span_class == other.span_class
16
+ end
17
+ end
18
+
19
+ class NumberField < Field
20
+ def initialize(title)
21
+ super(title, 'number', 'pull-right')
22
+ end
23
+ end
24
+
25
+ class StringField < Field
26
+ def initialize(title)
27
+ super(title, 'string', 'pull-left')
28
+ end
29
+ end
30
+
31
+ class Value
32
+ def initialize(val)
33
+ @val = val
34
+ end
35
+
36
+ def to_s
37
+ @val
38
+ end
39
+ end
40
+
41
+ class NumericValue < Value
42
+ def to_s
43
+ sprintf("%0.2f", @val)
44
+ end
45
+ end
46
+
47
+ class Report
48
+
49
+ attr_accessor :error, :fields, :rows
50
+
51
+ @@params = {}
52
+
53
+ def self.params=(params)
54
+ @@params = params
55
+ end
56
+
57
+ def self.params
58
+ @@params
59
+ end
60
+
61
+ def self.from_query(db, query)
62
+
63
+ @@params.each do |key, val|
64
+ params[key.to_sym] = val
65
+ end
66
+
67
+ ds = db.fetch(query, params)
68
+ report = self.new
69
+ begin
70
+ row = ds.first
71
+ if row.nil?
72
+ raise "No data"
73
+ end
74
+ ds.columns.each do |col|
75
+ value = row[col]
76
+ if value.is_a? Numeric
77
+ report.add_field NumberField.new(col.to_s)
78
+ else
79
+ report.add_field StringField.new(col.to_s)
80
+ end
81
+ end
82
+
83
+ ds.each do |row|
84
+ vals = []
85
+ ds.columns.each do |col|
86
+ vals << row[col]
87
+ end
88
+ report.add_row(vals)
89
+ end
90
+ rescue Exception => e
91
+ report.error = e
92
+ end
93
+
94
+ return report
95
+ end
96
+
97
+ def initialize
98
+ @fields = []
99
+ @rows = []
100
+ end
101
+
102
+ def add_field(field)
103
+ @fields << field
104
+ end
105
+
106
+ def add_row(row)
107
+ if row.length != @fields.length
108
+ raise "row length not equal to fields length"
109
+ end
110
+ @rows << row
111
+ end
112
+
113
+ def each_row
114
+ @rows.each do |row|
115
+ yield row.zip(@fields)
116
+ end
117
+ end
118
+
119
+ def pivot(column, sort_order)
120
+ new_report = self.class.new
121
+
122
+ bucket_column_index = 0
123
+ self.fields.each_with_index do |f, i|
124
+ if f.title == column
125
+ bucket_column_index = i
126
+ break
127
+ else
128
+ new_report.add_field(f)
129
+ end
130
+ end
131
+
132
+ buckets = {}
133
+ new_rows = {}
134
+
135
+ self.each_row do |row|
136
+ key = row[0, bucket_column_index].map { |r| r[0] }
137
+ bucket_name = row[bucket_column_index][0]
138
+ bucket_value = row[bucket_column_index + 1][0]
139
+
140
+ if not buckets.has_key? bucket_name
141
+ field = bucket_value.is_a?(Numeric) ? NumberField.new(bucket_name) : StringField.new(bucket_name)
142
+ buckets[bucket_name] = field
143
+ end
144
+
145
+ new_rows[key] ||= {}
146
+ new_rows[key][bucket_name] = bucket_value
147
+ end
148
+
149
+ bucket_keys = buckets.keys.sort
150
+ if sort_order && sort_order == 'desc'
151
+ bucket_keys = bucket_keys.reverse
152
+ end
153
+
154
+ bucket_keys.each do |bucket|
155
+ new_report.add_field(buckets[bucket])
156
+ end
157
+
158
+ new_rows.each do |key, value|
159
+ row = key
160
+ bucket_keys.each do |b|
161
+ row << value[b]
162
+ end
163
+
164
+ new_report.add_row(row)
165
+ end
166
+
167
+ return new_report
168
+ end
169
+ end
170
+
171
+ end
@@ -0,0 +1,5 @@
1
+ module Sequel
2
+ module Reporter
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ require "sequel-reporter/version"
2
+ require "sequel-reporter/cli"
3
+ require "sequel-reporter/report"
4
+ require "sequel-reporter/helpers"
5
+ require "sequel-reporter/application"
6
+
7
+ module Sequel
8
+ module Reporter
9
+ # Your code goes here...
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sequel-reporter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "sequel-reporter"
8
+ gem.version = Sequel::Reporter::VERSION
9
+ gem.authors = ["Pete Keen"]
10
+ gem.email = ["peter.keen@bugsplat.info"]
11
+ gem.description = %q{An opinionated framework for writing reporting applications}
12
+ gem.summary = %q{An opinionated framework for writing reporting applications}
13
+ gem.homepage = "https://github.com/peterkeen/sequel-reporter"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency('sqlite3')
21
+ gem.add_dependency('rspec')
22
+ gem.add_dependency('sinatra')
23
+ gem.add_dependency('sinatra-contrib')
24
+ gem.add_dependency('sequel')
25
+ end
@@ -0,0 +1,63 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
2
+
3
+ describe Sequel::Reporter::Report do
4
+
5
+ before :each do
6
+ @things.insert(:id => 1, :something => "hi", :other => "2013-02-18")
7
+ @things.insert(:id => 2, :something => "ho", :other => "2013-02-19")
8
+ end
9
+
10
+ describe "#from_query" do
11
+ it "should run a query" do
12
+ report = Sequel::Reporter::Report.from_query(@db, "select count(1) as foo from things")
13
+ rows = []
14
+ report.each_row do |row|
15
+ rows << row
16
+ end
17
+
18
+ rows[0][0][0].should eq(2)
19
+ rows[0][0][1].should eq(Sequel::Reporter::NumberField.new('foo'))
20
+ end
21
+
22
+ it "should use params" do
23
+ Sequel::Reporter::Report.params = {:blah => "hi"}
24
+ report = Sequel::Reporter::Report.from_query(@db, "select count(1) as foo from things where something = :blah")
25
+ rows = []
26
+ report.each_row do |row|
27
+ rows << row
28
+ end
29
+
30
+ rows[0][0][0].should eq(1)
31
+ end
32
+ end
33
+
34
+ describe "#pivot" do
35
+
36
+ before :each do
37
+ @things.insert(:id => 1, :something => "hi", :other => "2013-02-18")
38
+ @things.insert(:id => 2, :something => "ho", :other => "2013-02-19")
39
+ end
40
+
41
+ it "should create the correct fields" do
42
+ report = Sequel::Reporter::Report.from_query(@db, "select other, something, count(1) from things group by other, something")
43
+ report = report.pivot("something", "asc")
44
+
45
+ report.fields.should eq([
46
+ Sequel::Reporter::StringField.new('other'),
47
+ Sequel::Reporter::NumberField.new('hi'),
48
+ Sequel::Reporter::NumberField.new('ho'),
49
+ ])
50
+ end
51
+
52
+ it "should put the values in the right place" do
53
+ report = Sequel::Reporter::Report.from_query(@db, "select other, something, count(1) from things group by other, something")
54
+ report = report.pivot("something", "asc")
55
+
56
+ report.rows.should eq([
57
+ [Date.new(2013,2,18), 2, nil],
58
+ [Date.new(2013,2,19), nil, 2]
59
+ ])
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,16 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'rspec'
4
+ require 'sequel-reporter/application'
5
+ require 'sequel-reporter/report'
6
+ require 'sequel-reporter/helpers'
7
+ require 'sequel'
8
+ require 'sqlite3'
9
+
10
+ RSpec.configure do |config|
11
+ config.before(:each) do
12
+ @db = Sequel.sqlite
13
+ @db.run("create table things (id integer, something text, other date)")
14
+ @things = @db[:things]
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel-reporter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pete Keen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sqlite3
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: sinatra
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sinatra-contrib
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: sequel
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: An opinionated framework for writing reporting applications
95
+ email:
96
+ - peter.keen@bugsplat.info
97
+ executables:
98
+ - sequel-reporter
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - .gitignore
103
+ - Gemfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - bin/sequel-reporter
108
+ - data/Gemfile
109
+ - data/Rakefile.tt
110
+ - data/application.rb.tt
111
+ - data/config.ru
112
+ - data/lib/.gitkeep
113
+ - data/migrate/.gitkeep
114
+ - data/public/.gitkeep
115
+ - data/public/css/bootstrap.css
116
+ - data/public/css/bootstrap.min.css
117
+ - data/public/img/glyphicons-halflings-white.png
118
+ - data/public/img/glyphicons-halflings.png
119
+ - data/public/js/bootstrap.js
120
+ - data/public/js/bootstrap.min.js
121
+ - data/public/js/jquery-1.9.1.js
122
+ - data/views/error.erb
123
+ - data/views/layout.erb
124
+ - data/views/reports/dashboard.erb
125
+ - data/views/table.erb
126
+ - lib/sequel-reporter.rb
127
+ - lib/sequel-reporter/application.rb
128
+ - lib/sequel-reporter/cli.rb
129
+ - lib/sequel-reporter/helpers.rb
130
+ - lib/sequel-reporter/report.rb
131
+ - lib/sequel-reporter/version.rb
132
+ - sequel-reporter.gemspec
133
+ - test/report_spec.rb
134
+ - test/spec_helper.rb
135
+ homepage: https://github.com/peterkeen/sequel-reporter
136
+ licenses: []
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ! '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 1.8.23
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: An opinionated framework for writing reporting applications
159
+ test_files:
160
+ - test/report_spec.rb
161
+ - test/spec_helper.rb