dossier 2.7.1 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/{README.markdown → README.md} +10 -5
  3. data/Rakefile +1 -0
  4. data/app/controllers/dossier/reports_controller.rb +9 -38
  5. data/app/helpers/dossier/application_helper.rb +7 -1
  6. data/app/views/dossier/layouts/application.html.haml +6 -0
  7. data/app/views/dossier/reports/multi.html.haml +8 -3
  8. data/app/views/dossier/reports/show.html.haml +4 -2
  9. data/config/routes.rb +2 -2
  10. data/lib/dossier.rb +4 -8
  11. data/lib/dossier/adapter/active_record.rb +1 -1
  12. data/lib/dossier/formatter.rb +20 -3
  13. data/lib/dossier/multi_report.rb +17 -9
  14. data/lib/dossier/naming.rb +51 -0
  15. data/lib/dossier/renderer.rb +63 -0
  16. data/lib/dossier/report.rb +22 -7
  17. data/lib/dossier/responder.rb +34 -0
  18. data/lib/dossier/result.rb +5 -7
  19. data/lib/dossier/stream_csv.rb +10 -3
  20. data/lib/dossier/version.rb +1 -1
  21. data/lib/dossier/view_context_with_report_formatter.rb +7 -0
  22. data/lib/dossier/xls.rb +2 -2
  23. data/lib/generators/dossier/views/templates/show.html.haml +5 -3
  24. data/spec/dossier/adapter/active_record_spec.rb +1 -1
  25. data/spec/dossier/formatter_spec.rb +38 -3
  26. data/spec/dossier/multi_report_spec.rb +18 -1
  27. data/spec/dossier/naming_spec.rb +29 -0
  28. data/spec/dossier/renderer_spec.rb +57 -0
  29. data/spec/dossier/report_spec.rb +23 -2
  30. data/spec/dossier/responder_spec.rb +59 -0
  31. data/spec/dossier/result_spec.rb +4 -0
  32. data/spec/dossier/stream_csv_spec.rb +75 -0
  33. data/spec/dossier_spec.rb +0 -13
  34. data/spec/dummy/app/controllers/site_controller.rb +0 -4
  35. data/spec/dummy/app/reports/cats/are/super_fun_report.rb +9 -0
  36. data/spec/dummy/app/reports/combination_report.rb +9 -0
  37. data/spec/{support → dummy/app}/reports/employee_report.rb +0 -0
  38. data/spec/{support → dummy/app}/reports/employee_with_custom_client_report.rb +0 -0
  39. data/spec/dummy/app/reports/employee_with_custom_view_report.rb +27 -0
  40. data/spec/dummy/app/reports/hello_my_friends_report.rb +2 -0
  41. data/spec/{support → dummy/app}/reports/test_report.rb +0 -0
  42. data/spec/dummy/app/views/dossier/reports/combination/_options.html.haml +4 -0
  43. data/spec/dummy/app/views/dossier/reports/employee_with_custom_view.html.haml +5 -1
  44. data/spec/dummy/app/views/dossier/reports/employee_with_custom_view/_options.html.haml +6 -0
  45. data/spec/dummy/config/database.yml.travis +5 -0
  46. data/spec/dummy/db/test.sqlite3 +0 -0
  47. data/spec/dummy/log/development.log +3865 -0
  48. data/spec/dummy/log/test.log +44084 -0
  49. data/spec/dummy/tmp/cache/assets/CEA/5A0/sprockets%2Fc0534884cbc43494a05d9e957ea1298d +0 -0
  50. data/spec/dummy/tmp/cache/assets/D40/0D0/sprockets%2F15a6bb0a1346b6d7fe859c14bf729a49 +0 -0
  51. data/spec/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  52. data/spec/dummy/tmp/cache/assets/D51/510/sprockets%2Fca0353abc266080173bbc3c13efa935a +0 -0
  53. data/spec/dummy/tmp/cache/assets/D6C/400/sprockets%2F7fa180a6e05c7ca4346ef58c54bb30f8 +0 -0
  54. data/spec/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  55. data/spec/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  56. data/spec/dummy/{dossier_test → tmp/restart.txt} +0 -0
  57. data/spec/features/combination_report_spec.rb +13 -10
  58. data/spec/features/employee_spec.rb +76 -0
  59. data/spec/{requests → features}/employee_with_custom_client_spec.rb +3 -2
  60. data/spec/{requests → features}/employee_with_custom_controller_spec.rb +3 -2
  61. data/spec/features/namespaced_report_spec.rb +15 -0
  62. data/spec/fixtures/db/mysql2.yml.travis +4 -0
  63. data/spec/fixtures/db/sqlite3.yml.travis +2 -0
  64. data/spec/helpers/dossier/application_helper_spec.rb +24 -0
  65. data/spec/spec_helper.rb +11 -0
  66. metadata +99 -92
  67. data/spec/fixtures/reports/employee.html +0 -54
  68. data/spec/fixtures/reports/employee_with_custom_client.html +0 -54
  69. data/spec/fixtures/reports/employee_with_custom_view.html +0 -15
  70. data/spec/fixtures/reports/employee_with_footer.html +0 -56
  71. data/spec/fixtures/reports/employee_with_parameters.html +0 -38
  72. data/spec/requests/employee_spec.rb +0 -61
  73. data/spec/support/reports/combination_report.rb +0 -8
  74. data/spec/support/reports/employee_with_custom_view_report.rb +0 -8
  75. data/spec/support/reports/hello_my_friends_report.rb +0 -6
@@ -0,0 +1,34 @@
1
+ module Dossier
2
+ class Responder < ::ActionController::Responder
3
+ alias :report :resource
4
+
5
+ def to_html
6
+ report.renderer.engine = controller
7
+ controller.response_body = report.render
8
+ end
9
+
10
+ def to_json
11
+ controller.render json: report.results.hashes
12
+ end
13
+
14
+ def to_csv
15
+ set_content_disposition!
16
+ controller.response_body = StreamCSV.new(report.raw_results.arrays)
17
+ end
18
+
19
+ def to_xls
20
+ set_content_disposition!
21
+ controller.response_body = Xls.new(report.raw_results.arrays)
22
+ end
23
+
24
+ private
25
+
26
+ def set_content_disposition!
27
+ controller.headers["Content-Disposition"] = %[attachment;filename=#{filename}]
28
+ end
29
+
30
+ def filename
31
+ "#{report.class.filename}.#{format}"
32
+ end
33
+ end
34
+ end
@@ -10,15 +10,15 @@ module Dossier
10
10
  end
11
11
 
12
12
  def headers
13
- adapter_results.headers
13
+ @headers ||= adapter_results.headers
14
14
  end
15
15
 
16
16
  def body
17
- rows.first(rows.length - report.options[:footer].to_i)
17
+ @body ||= rows.first(rows.length - report.options[:footer].to_i)
18
18
  end
19
19
 
20
20
  def footers
21
- rows.last(report.options[:footer].to_i)
21
+ @footer ||= rows.last(report.options[:footer].to_i)
22
22
  end
23
23
 
24
24
  def rows
@@ -39,14 +39,12 @@ module Dossier
39
39
  end
40
40
 
41
41
  def each
42
- raise NotImplementedError, "Every result class must define `each`"
42
+ raise NotImplementedError, "#{self.class.name} must define `each`"
43
43
  end
44
44
 
45
45
  class Formatted < Result
46
46
  def each
47
- adapter_results.rows.each do |row|
48
- yield format(row)
49
- end
47
+ adapter_results.rows.each { |row| yield format(row) }
50
48
  end
51
49
 
52
50
  def format(row)
@@ -2,15 +2,16 @@ require 'csv'
2
2
 
3
3
  module Dossier
4
4
  class StreamCSV
5
+ attr_reader :headers, :collection
5
6
 
6
7
  def initialize(collection, headers = nil)
7
- @headers = headers || collection.shift
8
+ @headers = headers || collection.shift unless false === headers
8
9
  @collection = collection
9
10
  end
10
11
 
11
12
  def each
12
- yield @headers.map { |header| Dossier::Formatter.titleize(header) }.to_csv
13
- @collection.each do |record|
13
+ yield headers.map { |header| Dossier::Formatter.titleize(header) }.to_csv if headers?
14
+ collection.each do |record|
14
15
  yield record.to_csv
15
16
  end
16
17
  rescue => e
@@ -24,5 +25,11 @@ module Dossier
24
25
  end
25
26
  end
26
27
 
28
+ private
29
+
30
+ def headers?
31
+ headers.present?
32
+ end
33
+
27
34
  end
28
35
  end
@@ -1,3 +1,3 @@
1
1
  module Dossier
2
- VERSION = "2.7.1"
2
+ VERSION = "2.8.0"
3
3
  end
@@ -0,0 +1,7 @@
1
+ module Dossier
2
+ module ViewContextWithReportFormatter
3
+ def view_context
4
+ super.extend(report.formatter)
5
+ end
6
+ end
7
+ end
@@ -18,12 +18,12 @@ module Dossier
18
18
 
19
19
  private
20
20
 
21
- def as_cel(el)
21
+ def as_cell(el)
22
22
  %{<Cell><Data ss:Type="String">#{el}</Data></Cell>}
23
23
  end
24
24
 
25
25
  def as_row(array)
26
- my_array = array.map{|a| as_cel(a)}.join("\n")
26
+ my_array = array.map{|a| as_cell(a)}.join("\n")
27
27
  "<Row>\n" + my_array + "\n</Row>\n"
28
28
  end
29
29
 
@@ -1,12 +1,14 @@
1
- %h1= report.class.name.titleize
1
+ %h2= report.formatted_title
2
2
 
3
- = link_to 'Download CSV', dossier_report_path(format: 'csv', options: report.options, report: report.class.report_name), class: 'download-csv'
3
+ = link_to 'Download CSV', formatted_dossier_report_path('csv', report), class: 'download-csv'
4
+
5
+ = render_options(report)
4
6
 
5
7
  %table
6
8
  %thead
7
9
  %tr
8
10
  - report.results.headers.each do |header|
9
- %th= Dossier::Formatter.titleize(header)
11
+ %th= report.format_header(header)
10
12
  %tbody
11
13
  - report.results.body.each do |row|
12
14
  %tr
@@ -29,7 +29,7 @@ describe Dossier::Adapter::ActiveRecord do
29
29
  let(:adapter_result_class) { Dossier::Adapter::ActiveRecord::Result}
30
30
 
31
31
  it "delegates to the connection" do
32
- ar_connection.should_receive(:exec_query).with(query)
32
+ ar_connection.should_receive(:exec_query).with("\n#{query}")
33
33
  adapter.execute(query)
34
34
  end
35
35
 
@@ -24,15 +24,50 @@ describe Dossier::Formatter do
24
24
  let(:route) { {controller: :site, action: :index} }
25
25
 
26
26
  it "allows URL generation" do
27
- expect(formatter.url_for(route)).to eq('/woo')
27
+ expect(formatter.url_formatter.url_for(route)).to eq('/woo')
28
28
  end
29
29
 
30
30
  it "allows link generation" do
31
- expect(formatter.link_to('Woo!', route)).to eq('<a href="/woo">Woo!</a>')
31
+ expect(formatter.url_formatter.link_to('Woo!', route)).to eq('<a href="/woo">Woo!</a>')
32
32
  end
33
33
 
34
34
  it "allows usage of url helpers" do
35
- expect(formatter.url_helpers.woo_path).to eq('/woo')
35
+ expect(formatter.url_formatter.url_helpers.woo_path).to eq('/woo')
36
+ end
37
+ end
38
+
39
+ describe "custom formatters" do
40
+ describe "commafy_number" do
41
+ formats = {
42
+ 10_000 => '10,000',
43
+ 10_000.01 => '10,000.01',
44
+ 1_000_000_000.001 => '1,000,000,000.001',
45
+ '12345.6789' => '12,345.6789'
46
+ }.each { |base, formatted|
47
+ it "formats #{base} as #{formatted}" do
48
+ expect(formatter.commafy_number(base)).to eq formatted
49
+ end
50
+ }
51
+ it "will return the expected precision if too large" do
52
+ expect(formatter.commafy_number(1_000.23523563, 2)).to eq '1,000.24'
53
+ end
54
+
55
+ it "will return the expected precision if too small" do
56
+ expect(formatter.commafy_number(1_000, 5)).to eq '1,000.00000'
57
+ end
58
+ end
59
+
60
+ describe "number_to_dollars" do
61
+ formats = {
62
+ 10_000 => '$10,000.00',
63
+ 10_000.00 => '$10,000.00',
64
+ 1_000_000_000.000 => '$1,000,000,000.00',
65
+ '12345.6788' => '$12,345.68'
66
+ }.each { |base, formatted|
67
+ it "formats #{base} as #{formatted}" do
68
+ expect(formatter.number_to_dollars(base)).to eq formatted
69
+ end
70
+ }
36
71
  end
37
72
  end
38
73
 
@@ -2,10 +2,27 @@ require 'spec_helper'
2
2
 
3
3
  describe Dossier::MultiReport do
4
4
 
5
- let(:combined_report) {CombinationReport}
5
+ let(:options) { {'foo' => 'bar'} }
6
+ let(:combined_report) { CombinationReport }
7
+ let(:report) { combined_report.new(options) }
6
8
 
7
9
  it 'knows its sub reports' do
8
10
  expect(combined_report.reports).to eq([EmployeeReport, EmployeeWithCustomViewReport])
9
11
  end
10
12
 
13
+ it "passes options to the sub reports" do
14
+ combined_report.reports.each do |report|
15
+ report.should_receive(:new).with(options).and_call_original
16
+ end
17
+
18
+ report.reports
19
+ end
20
+
21
+ it "sets the multi property on its child reports" do
22
+ expect(report.reports.first.parent).to eq(report)
23
+ end
24
+
25
+ it "never has a parent" do
26
+ expect(report.parent).to be_nil
27
+ end
11
28
  end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Naming do
4
+ describe "report naming" do
5
+ let(:klass) { HelloMyFriendsReport }
6
+ let(:name) { 'hello_my_friends' }
7
+
8
+ it "converts a report class to a report name" do
9
+ expect(described_class.class_to_name(klass)).to eq(name)
10
+ end
11
+
12
+ it "converting a report name to a report class" do
13
+ expect(described_class.name_to_class(name)).to eq(klass)
14
+ end
15
+
16
+ describe "with namespaces" do
17
+ let(:klass) { Cats::Are::SuperFunReport }
18
+ let(:name) { 'cats/are/super_fun' }
19
+
20
+ it "converts a report class to a report name" do
21
+ expect(described_class.class_to_name klass).to eq name
22
+ end
23
+
24
+ it "converts a report name to a report class" do
25
+ expect(described_class.name_to_class name).to eq klass
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Renderer do
4
+
5
+ let(:report) { EmployeeReport.new }
6
+ let(:renderer) { described_class.new(report) }
7
+ let(:engine) { renderer.engine }
8
+
9
+ describe "rendering" do
10
+ let(:options) { {template: "dossier/reports/#{template}", locals: {report: report}} }
11
+
12
+ describe "with custom view" do
13
+ let(:report) { EmployeeWithCustomViewReport.new }
14
+ let(:template) { report.report_name }
15
+
16
+ it "renders the custom view" do
17
+ engine.should_receive(:render).with(options)
18
+ end
19
+ end
20
+
21
+ describe "without custom view" do
22
+ let(:template) { 'show' }
23
+
24
+ it "renders show" do
25
+ engine.should_receive(:render).with(options.merge(template: 'dossier/reports/employee')).and_call_original
26
+ engine.should_receive(:render).with(options)
27
+ end
28
+ end
29
+
30
+ after(:each) { renderer.render }
31
+ end
32
+
33
+ describe "engine" do
34
+ describe "view_context" do
35
+ it "mixes in the dossier/application_helper to that view context" do
36
+ expect(engine.view_context.class.ancestors).to include(Dossier::ApplicationHelper)
37
+ end
38
+ end
39
+
40
+ describe "view path" do
41
+ it "has the same view paths the application would have" do
42
+ extractor = ->(vp) { vp.paths }
43
+ expect(extractor.call engine.view_paths).to eq(extractor.call ActionController::Base.view_paths)
44
+ end
45
+ end
46
+
47
+ describe "layouts" do
48
+ it "uses a layout" do
49
+ expect(report.render).to match('<html>')
50
+ end
51
+
52
+ it "makes the report available to the layout" do
53
+ expect(report.render).to match('<title>Employee Report</title>')
54
+ end
55
+ end
56
+ end
57
+ end
@@ -8,6 +8,15 @@ describe Dossier::Report do
8
8
  TestReport.report_name.should eq('test')
9
9
  end
10
10
 
11
+ it "has a template name that is the report name" do
12
+ expect(report.template).to eq(report.report_name)
13
+ end
14
+
15
+ it "allows overriding the template" do
16
+ report = Class.new(described_class) { def template; 'fooo'; end }
17
+ expect(report.new.template).to eq 'fooo'
18
+ end
19
+
11
20
  describe "report instances" do
12
21
  let(:report_with_custom_header) do
13
22
  Class.new(Dossier::Report) do
@@ -20,18 +29,20 @@ describe Dossier::Report do
20
29
  end
21
30
 
22
31
  it "takes options when initializing" do
23
- report = TestReport.new(:foo => 'bar')
24
32
  report.options.should eq('foo' => 'bar')
25
33
  end
26
34
 
27
35
  it 'generates column headers' do
28
- report = TestReport.new(:foo => 'bar')
29
36
  report.format_header('Foo').should eq 'Foo'
30
37
  end
31
38
 
32
39
  it 'allows for column header customization' do
33
40
  report_with_custom_header.format_header(:generic).should eq 'customized'
34
41
  end
42
+
43
+ it "has a formatted title" do
44
+ expect(report.formatted_title).to eq 'Test Report'
45
+ end
35
46
  end
36
47
 
37
48
  describe "callbacks" do
@@ -81,4 +92,14 @@ describe Dossier::Report do
81
92
 
82
93
  end
83
94
 
95
+ describe "rendering" do
96
+ it "has a renderer" do
97
+ expect(report.renderer).to be_a(Dossier::Renderer)
98
+ end
99
+
100
+ it "delegates render to the renderer" do
101
+ report.renderer.should_receive(:render)
102
+ report.render
103
+ end
104
+ end
84
105
  end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dossier::Responder do
4
+
5
+ def stub_out_report_results(report)
6
+ report.tap { |r|
7
+ r.stub(:results).and_return(results)
8
+ r.stub(:raw_results).and_return(results)
9
+ }
10
+ end
11
+
12
+ let(:results) { mock(arrays: [[]], hashes: [{}]) }
13
+ let(:report) { EmployeeReport.new }
14
+ let(:reports) { [stub_out_report_results(report)] }
15
+ let(:controller) {
16
+ ActionController::Base.new.tap { |controller| controller.stub(:headers).and_return({}) }
17
+ }
18
+ let(:responder) { described_class.new(controller, reports, {}) }
19
+
20
+ describe "to_html" do
21
+ it "calls render on the report" do
22
+ report.should_receive(:render)
23
+ responder.to_html
24
+ end
25
+ end
26
+
27
+ describe "to_json" do
28
+ it "renders the report as json" do
29
+ controller.should_receive(:render).with(json: results.hashes)
30
+ responder.to_json
31
+ end
32
+ end
33
+
34
+ describe "to_csv" do
35
+ it "sets the content disposition" do
36
+ responder.should_receive(:set_content_disposition!)
37
+ responder.to_csv
38
+ end
39
+
40
+ it "sets the response body to a new csv streamer instance" do
41
+ responder.to_csv
42
+ expect(responder.controller.response_body).to be_a(Dossier::StreamCSV)
43
+ end
44
+ end
45
+
46
+ describe "to_xls" do
47
+ it "sets the content disposition" do
48
+ responder.should_receive(:set_content_disposition!)
49
+ responder.to_xls
50
+ end
51
+
52
+ it "sets the response body to a new xls instance" do
53
+ responder.to_xls
54
+ expect(responder.controller.response_body).to be_a(Dossier::Xls)
55
+ end
56
+ end
57
+
58
+ end
59
+