dossier 2.7.1 → 2.8.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 (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
+