dossier 2.0.0

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.
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