sequel-reporter 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.
@@ -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