dossier 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.markdown +181 -0
  3. data/Rakefile +27 -0
  4. data/app/assets/javascripts/dossier/application.js +15 -0
  5. data/app/assets/stylesheets/dossier/application.css +13 -0
  6. data/app/controllers/dossier/application_controller.rb +4 -0
  7. data/app/controllers/dossier/reports_controller.rb +34 -0
  8. data/app/helpers/dossier/application_helper.rb +4 -0
  9. data/app/views/dossier/reports/show.html.haml +21 -0
  10. data/config/routes.rb +5 -0
  11. data/lib/dossier.rb +31 -0
  12. data/lib/dossier/adapter/active_record.rb +40 -0
  13. data/lib/dossier/adapter/active_record/result.rb +24 -0
  14. data/lib/dossier/client.rb +52 -0
  15. data/lib/dossier/configuration.rb +28 -0
  16. data/lib/dossier/engine.rb +7 -0
  17. data/lib/dossier/formatter.rb +33 -0
  18. data/lib/dossier/query.rb +30 -0
  19. data/lib/dossier/report.rb +69 -0
  20. data/lib/dossier/result.rb +67 -0
  21. data/lib/dossier/stream_csv.rb +24 -0
  22. data/lib/dossier/version.rb +3 -0
  23. data/lib/tasks/dossier_tasks.rake +4 -0
  24. data/spec/dossier/adapter/active_record/result_spec.rb +31 -0
  25. data/spec/dossier/adapter/active_record_spec.rb +54 -0
  26. data/spec/dossier/client_spec.rb +109 -0
  27. data/spec/dossier/configuration_spec.rb +35 -0
  28. data/spec/dossier/formatter_spec.rb +39 -0
  29. data/spec/dossier/query_spec.rb +59 -0
  30. data/spec/dossier/report_spec.rb +67 -0
  31. data/spec/dossier/result_spec.rb +119 -0
  32. data/spec/dossier_spec.rb +31 -0
  33. data/spec/dummy/README.rdoc +261 -0
  34. data/spec/dummy/Rakefile +7 -0
  35. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  36. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  37. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  38. data/spec/dummy/app/controllers/site_controller.rb +5 -0
  39. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  40. data/spec/dummy/app/views/dossier/reports/suspended_employee.html.haml +1 -0
  41. data/spec/dummy/app/views/dossier/reports/total.html.haml +11 -0
  42. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  43. data/spec/dummy/config.ru +4 -0
  44. data/spec/dummy/config/application.rb +56 -0
  45. data/spec/dummy/config/boot.rb +10 -0
  46. data/spec/dummy/config/database.yml.example +13 -0
  47. data/spec/dummy/config/environment.rb +9 -0
  48. data/spec/dummy/config/environments/development.rb +37 -0
  49. data/spec/dummy/config/environments/production.rb +67 -0
  50. data/spec/dummy/config/environments/test.rb +37 -0
  51. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  52. data/spec/dummy/config/initializers/inflections.rb +15 -0
  53. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  54. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  55. data/spec/dummy/config/initializers/session_store.rb +8 -0
  56. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  57. data/spec/dummy/config/locales/en.yml +5 -0
  58. data/spec/dummy/config/routes.rb +5 -0
  59. data/spec/dummy/config/setup_load_paths.rb +15 -0
  60. data/spec/dummy/db/schema.rb +24 -0
  61. data/spec/dummy/dossier_test +0 -0
  62. data/spec/dummy/public/404.html +26 -0
  63. data/spec/dummy/public/422.html +26 -0
  64. data/spec/dummy/public/500.html +25 -0
  65. data/spec/dummy/public/favicon.ico +0 -0
  66. data/spec/dummy/script/rails +6 -0
  67. data/spec/fixtures/customized_employee_report.html +38 -0
  68. data/spec/fixtures/db/mysql2.yml.example +4 -0
  69. data/spec/fixtures/db/sqlite3.yml.example +2 -0
  70. data/spec/fixtures/employee_report.csv +4 -0
  71. data/spec/fixtures/employee_report.html +54 -0
  72. data/spec/fixtures/employee_report_with_footer.html +56 -0
  73. data/spec/fixtures/employee_with_custom_client.html +54 -0
  74. data/spec/requests/employee_report_spec.rb +52 -0
  75. data/spec/requests/employee_with_custom_client_spec.rb +13 -0
  76. data/spec/routing/dossier_routes_spec.rb +11 -0
  77. data/spec/spec_helper.rb +36 -0
  78. data/spec/support/factory.rb +86 -0
  79. data/spec/support/reports/employee_report.rb +78 -0
  80. data/spec/support/reports/employee_with_custom_client.rb +10 -0
  81. data/spec/support/reports/sqlite_employee_report.rb +15 -0
  82. data/spec/support/reports/supended_employee_report.rb +7 -0
  83. data/spec/support/reports/test_report.rb +4 -0
  84. data/spec/support/reports/total_report.rb +35 -0
  85. metadata +361 -0
@@ -0,0 +1,28 @@
1
+ require 'yaml'
2
+
3
+ module Dossier
4
+ class Configuration
5
+
6
+ attr_accessor :config_path, :connection_options, :client
7
+
8
+ def initialize
9
+ @config_path = Rails.root.join('config', 'dossier.yml')
10
+ setup_client!
11
+ end
12
+
13
+ private
14
+
15
+ def setup_client!
16
+ @connection_options = YAML.load_file(@config_path)[Rails.env].symbolize_keys
17
+ @client = Dossier::Client.new(@connection_options)
18
+
19
+ rescue Errno::ENOENT => e
20
+ raise ConfigurationMissingError.new(
21
+ "#{e.message}. #{@config_path} must exist for Dossier to connect to the database."
22
+ )
23
+ end
24
+
25
+ end
26
+
27
+ class ConfigurationMissingError < StandardError ; end
28
+ end
@@ -0,0 +1,7 @@
1
+ require 'rails'
2
+ require 'haml'
3
+
4
+ module Dossier
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ module Dossier
2
+ module Formatter
3
+ extend self
4
+ extend ActiveSupport::Inflector
5
+ extend ActionView::Helpers::NumberHelper
6
+
7
+ def number_to_currency_from_cents(value)
8
+ number_to_currency(value /= 100.0)
9
+ end
10
+
11
+ def url_formatter
12
+ @url_formatter ||= UrlFormatter.new
13
+ end
14
+
15
+ delegate :url_for, :link_to, :url_helpers, to: :url_formatter
16
+
17
+ class UrlFormatter
18
+ include ActionView::Helpers::UrlHelper
19
+
20
+ def _routes
21
+ Rails.application.routes
22
+ end
23
+
24
+ # No controller in current context, must be specified when generating routes
25
+ def controller
26
+ end
27
+
28
+ def url_helpers
29
+ _routes.url_helpers
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Dossier
2
+ class Query
3
+
4
+ attr_reader :string, :report
5
+
6
+ def initialize(report)
7
+ @report = report
8
+ @string = report.sql.dup
9
+ end
10
+
11
+ def to_s
12
+ compile
13
+ end
14
+
15
+ private
16
+
17
+ def compile
18
+ string.gsub(/\w*\:\w+/) { |match| escape(report.public_send(match[1..-1])) }
19
+ end
20
+
21
+ def escape(value)
22
+ if value.respond_to?(:map)
23
+ "(#{value.map { |v| escape(v) }.join(', ')})"
24
+ else
25
+ report.dossier_client.escape(value)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,69 @@
1
+ module Dossier
2
+ class Report
3
+ include ActiveSupport::Callbacks
4
+ extend ActiveModel::Naming
5
+
6
+ define_callbacks :build_query, :execute
7
+
8
+ attr_reader :options
9
+
10
+ def initialize(options = {})
11
+ @options = options.dup.with_indifferent_access
12
+ end
13
+
14
+ def sql
15
+ raise NotImplementedError, "`sql` method must be defined by each report"
16
+ end
17
+
18
+ def query
19
+ build_query unless defined?(@query)
20
+ @query.to_s
21
+ end
22
+
23
+ def results
24
+ execute unless defined?(@results)
25
+ @results
26
+ end
27
+
28
+ def raw_results
29
+ execute unless defined?(@raw_results)
30
+ @raw_results
31
+ end
32
+
33
+ def run
34
+ tap { execute }
35
+ end
36
+
37
+ def view
38
+ self.class.name.sub(/Report\Z/, '').underscore
39
+ end
40
+
41
+ def formatter
42
+ Dossier::Formatter
43
+ end
44
+
45
+ def dossier_client
46
+ Dossier.client
47
+ end
48
+
49
+ private
50
+
51
+ def build_query
52
+ run_callbacks(:build_query) { @query = Dossier::Query.new(self) }
53
+ end
54
+
55
+ def execute
56
+ build_query
57
+ run_callbacks :execute do
58
+ self.results = dossier_client.execute(query, self.class.name)
59
+ end
60
+ end
61
+
62
+ def results=(results)
63
+ results.freeze
64
+ @raw_results = Result::Unformatted.new(results, self)
65
+ @results = Result::Formatted.new(results, self)
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,67 @@
1
+ module Dossier
2
+ class Result
3
+ include Enumerable
4
+
5
+ attr_accessor :report, :adapter_results
6
+
7
+ def initialize(adapter_results, report)
8
+ self.adapter_results = adapter_results
9
+ self.report = report
10
+ end
11
+
12
+ def headers
13
+ adapter_results.headers
14
+ end
15
+
16
+ def body
17
+ rows.first(rows.length - report.options[:footer].to_i)
18
+ end
19
+
20
+ def footers
21
+ rows.last(report.options[:footer].to_i)
22
+ end
23
+
24
+ def rows
25
+ @rows ||= to_a
26
+ end
27
+
28
+ def arrays
29
+ @arrays ||= [headers] + rows
30
+ end
31
+
32
+ def hashes
33
+ return @hashes if defined?(@hashes)
34
+ @hashes = rows.map { |row| Hash[headers.zip(row)] }
35
+ end
36
+
37
+ def each
38
+ raise NotImplementedError, "Every result class must define `each`"
39
+ end
40
+
41
+ class Formatted < Result
42
+ def each
43
+ adapter_results.rows.each do |row|
44
+ yield format(row)
45
+ end
46
+ end
47
+
48
+ def format(result_row)
49
+ unless result_row.kind_of?(Enumerable)
50
+ raise ArgumentError.new("#{result_row.inspect} must be a kind of Enumerable")
51
+ end
52
+
53
+ result_row.each_with_index.map do |field, i|
54
+ method = "format_#{headers[i]}"
55
+ report.respond_to?(method) ? report.public_send(method, field) : field
56
+ end
57
+ end
58
+ end
59
+
60
+ class Unformatted < Result
61
+ def each
62
+ adapter_results.rows.each { |row| yield row }
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ require 'csv'
2
+
3
+ module Dossier
4
+ class StreamCSV
5
+
6
+ def initialize(collection, headers = nil)
7
+ @headers = headers || collection.shift
8
+ @collection = collection
9
+ end
10
+
11
+ def each
12
+ yield @headers.map { |header| Dossier::Formatter.titleize(header) }.to_csv
13
+ @collection.each do |record|
14
+ yield record.to_csv
15
+ end
16
+ rescue => e
17
+ yield e.message
18
+ e.backtrace.each do |line|
19
+ yield "#{line}\n"
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module Dossier
2
+ VERSION = "2.0.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :dossier do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Adapter::ActiveRecord::Result do
4
+
5
+ let(:ar_connection_results) { double(:results, columns: %w[name age], rows: [['bob', 20], ['sue', 30]]) }
6
+ let(:result) { described_class.new(ar_connection_results) }
7
+
8
+ describe "headers" do
9
+
10
+ let(:fake_columns) { %[foo bar] }
11
+
12
+ it "calls `columns` on its connection_results" do
13
+ ar_connection_results.should_receive(:columns)
14
+ result.headers
15
+ end
16
+
17
+ it "returns the columns from the connection_results" do
18
+ expect(result.headers).to eq(ar_connection_results.columns)
19
+ end
20
+
21
+ end
22
+
23
+ describe "rows" do
24
+
25
+ it "returns the connection_results" do
26
+ expect(result.rows).to eq(ar_connection_results.rows)
27
+ end
28
+
29
+ end
30
+ end
31
+
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Adapter::ActiveRecord do
4
+
5
+ let(:ar_connection) { double(:activerecord_connection) }
6
+ let(:adapter) { described_class.new({connection: ar_connection}) }
7
+
8
+ describe "escaping" do
9
+
10
+ let(:dirty_value) { "Robert'); DROP TABLE Students;--" }
11
+ let(:clean_value) { "'Robert\\'); DROP TABLE Students;--'" }
12
+
13
+ it "delegates to the connection" do
14
+ ar_connection.should_receive(:quote).with(dirty_value)
15
+ adapter.escape(dirty_value)
16
+ end
17
+
18
+ it "returns the connection's escaped value" do
19
+ ar_connection.stub(:quote).and_return(clean_value)
20
+ expect(adapter.escape(dirty_value)).to eq(clean_value)
21
+ end
22
+
23
+ end
24
+
25
+ describe "execution" do
26
+
27
+ let(:query) { 'SELECT * FROM `people_who_resemble_vladimir_putin`' }
28
+ let(:connection_results) { [] }
29
+ let(:adapter_result_class) { Dossier::Adapter::ActiveRecord::Result}
30
+
31
+ it "delegates to the connection" do
32
+ ar_connection.should_receive(:exec_query).with(query)
33
+ adapter.execute(query)
34
+ end
35
+
36
+ it "builds an adapter result" do
37
+ ar_connection.stub(:exec_query).and_return(connection_results)
38
+ adapter_result_class.should_receive(:new).with(connection_results)
39
+ adapter.execute(:query)
40
+ end
41
+
42
+ it "returns the adapter result" do
43
+ ar_connection.stub(:exec_query).and_return(connection_results)
44
+ expect(adapter.execute(:query)).to be_a(adapter_result_class)
45
+ end
46
+
47
+ it "rescues any errors and raises a Dossier::ExecuteError" do
48
+ ar_connection.stub(:exec_query).and_raise(StandardError.new('wat'))
49
+ expect{ adapter.execute(:query) }.to raise_error(Dossier::ExecuteError)
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Client do
4
+
5
+ let(:connection) {
6
+ double(:connection, class: double(:class, name: 'ActiveRecord::ConnectionAdapters::Mysql2Adapter'))
7
+ }
8
+
9
+ describe "initialization" do
10
+
11
+ describe "finding the correct adapter" do
12
+
13
+ context "when given a connection object" do
14
+
15
+ let(:client) { described_class.new(connection: connection) }
16
+
17
+ it "determines the adapter from the connection's class" do
18
+ expect(client.adapter).to be_a(Dossier::Adapter::ActiveRecord)
19
+ end
20
+
21
+ end
22
+
23
+ context "when given a dossier_adapter option" do
24
+
25
+ before :each do
26
+ Dossier::Adapter::SpecAdapter = Struct.new(:options)
27
+ end
28
+
29
+ after :each do
30
+ Dossier::Adapter.send(:remove_const, :SpecAdapter)
31
+ end
32
+
33
+ it "uses an adapter by that name" do
34
+ Dossier::Adapter::SpecAdapter.should_receive(:new).with(username: 'Timmy')
35
+ described_class.new(dossier_adapter: 'spec_adapter', username: 'Timmy')
36
+ end
37
+
38
+ end
39
+
40
+ context "when not given a connection or a dossier_adapter option" do
41
+
42
+ let(:client) { described_class.new(username: 'Jimmy') }
43
+
44
+ describe "if there is one known ORM loaded" do
45
+
46
+ before :each do
47
+ described_class.any_instance.stub(:loaded_orms).and_return([double(:class, name: 'ActiveRecord::Base')])
48
+ end
49
+
50
+ it "uses that ORM's adapter" do
51
+ Dossier::Adapter::ActiveRecord.should_receive(:new).with(username: 'Jimmy')
52
+ described_class.new(username: 'Jimmy')
53
+ end
54
+
55
+ end
56
+
57
+ context "if there are no known ORMs loaded" do
58
+
59
+ before :each do
60
+ described_class.any_instance.stub(:loaded_orms).and_return([])
61
+ end
62
+
63
+ it "raises an error" do
64
+ expect{described_class.new(username: 'Jimmy')}.to raise_error(Dossier::Client::IndeterminableAdapter)
65
+ end
66
+
67
+ end
68
+
69
+ describe "if there are multiple known ORMs loaded" do
70
+
71
+ before :each do
72
+ described_class.any_instance.stub(:loaded_orms).and_return([:orm1, :orm2])
73
+ end
74
+
75
+ it "raises an error" do
76
+ expect{described_class.new(username: 'Jimmy')}.to raise_error(Dossier::Client::IndeterminableAdapter)
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ describe "instances" do
88
+
89
+ let(:client) { described_class.new(connection: connection) }
90
+ let(:adapter) { double(:adapter) }
91
+
92
+ before :each do
93
+ client.stub(:adapter).and_return(adapter)
94
+ end
95
+
96
+ it "delegates `escape` to its adapter" do
97
+ adapter.should_receive(:escape).with('Bobby Tables')
98
+ client.escape('Bobby Tables')
99
+ end
100
+
101
+ it "delegates `execute` to its adapter" do
102
+ adapter.should_receive(:execute).with('SELECT * FROM `primes`') # It's OK, it's in the cloud!
103
+ client.execute('SELECT * FROM `primes`')
104
+ end
105
+
106
+
107
+ end
108
+
109
+ end