dossier 2.7.1 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/{README.markdown → README.md} +10 -5
- data/Rakefile +1 -0
- data/app/controllers/dossier/reports_controller.rb +9 -38
- data/app/helpers/dossier/application_helper.rb +7 -1
- data/app/views/dossier/layouts/application.html.haml +6 -0
- data/app/views/dossier/reports/multi.html.haml +8 -3
- data/app/views/dossier/reports/show.html.haml +4 -2
- data/config/routes.rb +2 -2
- data/lib/dossier.rb +4 -8
- data/lib/dossier/adapter/active_record.rb +1 -1
- data/lib/dossier/formatter.rb +20 -3
- data/lib/dossier/multi_report.rb +17 -9
- data/lib/dossier/naming.rb +51 -0
- data/lib/dossier/renderer.rb +63 -0
- data/lib/dossier/report.rb +22 -7
- data/lib/dossier/responder.rb +34 -0
- data/lib/dossier/result.rb +5 -7
- data/lib/dossier/stream_csv.rb +10 -3
- data/lib/dossier/version.rb +1 -1
- data/lib/dossier/view_context_with_report_formatter.rb +7 -0
- data/lib/dossier/xls.rb +2 -2
- data/lib/generators/dossier/views/templates/show.html.haml +5 -3
- data/spec/dossier/adapter/active_record_spec.rb +1 -1
- data/spec/dossier/formatter_spec.rb +38 -3
- data/spec/dossier/multi_report_spec.rb +18 -1
- data/spec/dossier/naming_spec.rb +29 -0
- data/spec/dossier/renderer_spec.rb +57 -0
- data/spec/dossier/report_spec.rb +23 -2
- data/spec/dossier/responder_spec.rb +59 -0
- data/spec/dossier/result_spec.rb +4 -0
- data/spec/dossier/stream_csv_spec.rb +75 -0
- data/spec/dossier_spec.rb +0 -13
- data/spec/dummy/app/controllers/site_controller.rb +0 -4
- data/spec/dummy/app/reports/cats/are/super_fun_report.rb +9 -0
- data/spec/dummy/app/reports/combination_report.rb +9 -0
- data/spec/{support → dummy/app}/reports/employee_report.rb +0 -0
- data/spec/{support → dummy/app}/reports/employee_with_custom_client_report.rb +0 -0
- data/spec/dummy/app/reports/employee_with_custom_view_report.rb +27 -0
- data/spec/dummy/app/reports/hello_my_friends_report.rb +2 -0
- data/spec/{support → dummy/app}/reports/test_report.rb +0 -0
- data/spec/dummy/app/views/dossier/reports/combination/_options.html.haml +4 -0
- data/spec/dummy/app/views/dossier/reports/employee_with_custom_view.html.haml +5 -1
- data/spec/dummy/app/views/dossier/reports/employee_with_custom_view/_options.html.haml +6 -0
- data/spec/dummy/config/database.yml.travis +5 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +3865 -0
- data/spec/dummy/log/test.log +44084 -0
- data/spec/dummy/tmp/cache/assets/CEA/5A0/sprockets%2Fc0534884cbc43494a05d9e957ea1298d +0 -0
- data/spec/dummy/tmp/cache/assets/D40/0D0/sprockets%2F15a6bb0a1346b6d7fe859c14bf729a49 +0 -0
- data/spec/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/spec/dummy/tmp/cache/assets/D51/510/sprockets%2Fca0353abc266080173bbc3c13efa935a +0 -0
- data/spec/dummy/tmp/cache/assets/D6C/400/sprockets%2F7fa180a6e05c7ca4346ef58c54bb30f8 +0 -0
- data/spec/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/spec/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/spec/dummy/{dossier_test → tmp/restart.txt} +0 -0
- data/spec/features/combination_report_spec.rb +13 -10
- data/spec/features/employee_spec.rb +76 -0
- data/spec/{requests → features}/employee_with_custom_client_spec.rb +3 -2
- data/spec/{requests → features}/employee_with_custom_controller_spec.rb +3 -2
- data/spec/features/namespaced_report_spec.rb +15 -0
- data/spec/fixtures/db/mysql2.yml.travis +4 -0
- data/spec/fixtures/db/sqlite3.yml.travis +2 -0
- data/spec/helpers/dossier/application_helper_spec.rb +24 -0
- data/spec/spec_helper.rb +11 -0
- metadata +99 -92
- data/spec/fixtures/reports/employee.html +0 -54
- data/spec/fixtures/reports/employee_with_custom_client.html +0 -54
- data/spec/fixtures/reports/employee_with_custom_view.html +0 -15
- data/spec/fixtures/reports/employee_with_footer.html +0 -56
- data/spec/fixtures/reports/employee_with_parameters.html +0 -38
- data/spec/requests/employee_spec.rb +0 -61
- data/spec/support/reports/combination_report.rb +0 -8
- data/spec/support/reports/employee_with_custom_view_report.rb +0 -8
- data/spec/support/reports/hello_my_friends_report.rb +0 -6
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b964aa00e953df017cd8772cd0ed975301b841bd
|
4
|
+
data.tar.gz: c8faa33c28dec8b77a253918d089eea36d41c06d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4557b660e38d7b8a6b8aeb4bd7a7579ae0bc5eeea214bf3a6a02bd45330b95bd120bacb8cf9905cf5c4bf47922f6f37f7d57ba2ffff6bc8e660867556e02653a
|
7
|
+
data.tar.gz: 27c413936b6b22c1c4f977eb7546419b92f8804df1908e6346b82ce3f3f74e0bdcc42b182d7300a799b2f43c0658a25d7d12c8c869455036ef0b0ae6e292c1a4
|
@@ -5,7 +5,11 @@ Dossier is a Rails engine that turns SQL into reports. Reports can be easily ren
|
|
5
5
|
- If you **hate** SQL, you can use whatever tool you like to generate it; for example, ActiveRecord's `to_sql`.
|
6
6
|
- If you **love** SQL, you can use every feature your database supports.
|
7
7
|
|
8
|
+
[![Gem Version](https://badge.fury.io/rb/dossier.png)](https://rubygems.org/gems/dossier)
|
8
9
|
[![Code Climate](https://codeclimate.com/github/adamhunter/dossier.png)](https://codeclimate.com/github/adamhunter/dossier)
|
10
|
+
[![Build Status](https://travis-ci.org/adamhunter/dossier.png?branch=master)](https://travis-ci.org/adamhunter/dossier)
|
11
|
+
[![Coverage Status](https://coveralls.io/repos/adamhunter/dossier/badge.png?branch=master)](https://coveralls.io/r/adamhunter/dossier?branch=master)
|
12
|
+
[![Dependency Status](https://gemnasium.com/adamhunter/dossier.png)](https://gemnasium.com/adamhunter/dossier)
|
9
13
|
|
10
14
|
## Setup
|
11
15
|
|
@@ -75,10 +79,11 @@ By default, headers are generated by calling `titleize` on the column name from
|
|
75
79
|
class ProductMarginReport < Dossier::Report
|
76
80
|
# ...
|
77
81
|
def format_header(column_name)
|
78
|
-
{
|
79
|
-
|
80
|
-
|
81
|
-
}
|
82
|
+
custom_headers = {
|
83
|
+
margin_percentage: 'Margin %',
|
84
|
+
absolute_margin: 'Margin $'
|
85
|
+
}
|
86
|
+
custom_headers.fetch(column_name.to_sym) { super }
|
82
87
|
end
|
83
88
|
end
|
84
89
|
```
|
@@ -206,7 +211,7 @@ end
|
|
206
211
|
### Dossier for APIs
|
207
212
|
|
208
213
|
```ruby
|
209
|
-
class
|
214
|
+
class Api::ProjectsController < Api::ApplicationController
|
210
215
|
|
211
216
|
def snapshot
|
212
217
|
render json: ProjectStatusReport.new(project: @project).results.hashes
|
data/Rakefile
CHANGED
@@ -1,51 +1,23 @@
|
|
1
1
|
module Dossier
|
2
2
|
class ReportsController < ApplicationController
|
3
|
+
include ViewContextWithReportFormatter
|
4
|
+
|
5
|
+
self.responder = Dossier::Responder
|
6
|
+
|
7
|
+
respond_to :html, :json, :csv, :xls, only: :show
|
8
|
+
|
3
9
|
def show
|
4
|
-
|
5
|
-
format.html do
|
6
|
-
begin
|
7
|
-
render template: "dossier/reports/#{report_class.report_name}", locals: {report: report}
|
8
|
-
rescue ActionView::MissingTemplate => e
|
9
|
-
render template: 'dossier/reports/show', locals: {report: report}
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
format.json do
|
14
|
-
render :json => report.results.hashes
|
15
|
-
end
|
16
|
-
|
17
|
-
format.csv do
|
18
|
-
set_content_disposition!
|
19
|
-
self.response_body = StreamCSV.new(report.raw_results.arrays)
|
20
|
-
end
|
21
|
-
|
22
|
-
format.xls do
|
23
|
-
set_content_disposition!
|
24
|
-
self.response_body = Xls.new(report.raw_results.arrays)
|
25
|
-
end
|
26
|
-
end
|
10
|
+
respond_with(report)
|
27
11
|
end
|
28
12
|
|
29
13
|
def multi
|
30
|
-
|
31
|
-
format.html do
|
32
|
-
begin
|
33
|
-
render template: "dossier/reports/#{report_class.report_name}", locals: {multi: report}
|
34
|
-
rescue ActionView::MissingTemplate => e
|
35
|
-
render template: 'dossier/reports/multi', locals: {multi: report}
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
14
|
+
render template: 'dossier/reports/multi', locals: {multi: report}
|
39
15
|
end
|
40
16
|
|
41
17
|
private
|
42
18
|
|
43
19
|
def report_class
|
44
|
-
Dossier.name_to_class(params[:report])
|
45
|
-
end
|
46
|
-
|
47
|
-
def set_content_disposition!
|
48
|
-
headers["Content-Disposition"] = %[attachment;filename=#{params[:report]}-report_#{Time.now.strftime('%m-%d-%Y_%H-%M-%S')}.#{params[:format]}]
|
20
|
+
Dossier::Naming.name_to_class(params[:report])
|
49
21
|
end
|
50
22
|
|
51
23
|
def report
|
@@ -55,6 +27,5 @@ module Dossier
|
|
55
27
|
def options_params
|
56
28
|
params[:options].presence || {}
|
57
29
|
end
|
58
|
-
|
59
30
|
end
|
60
31
|
end
|
@@ -2,7 +2,13 @@ module Dossier
|
|
2
2
|
module ApplicationHelper
|
3
3
|
|
4
4
|
def formatted_dossier_report_path(format, report)
|
5
|
-
dossier_report_path(format: format, options: report.options, report: report.
|
5
|
+
dossier_report_path(format: format, options: report.options, report: report.report_name)
|
6
|
+
end
|
7
|
+
|
8
|
+
def render_options(report)
|
9
|
+
return if report.parent
|
10
|
+
render "dossier/reports/#{report.report_name}/options", report: report
|
11
|
+
rescue ActionView::MissingTemplate
|
6
12
|
end
|
7
13
|
|
8
14
|
end
|
@@ -1,3 +1,8 @@
|
|
1
|
-
%
|
2
|
-
-
|
3
|
-
|
1
|
+
%div{id: multi.dom_id}
|
2
|
+
%h1.dossier-multi-header
|
3
|
+
= multi.formatted_title
|
4
|
+
|
5
|
+
= render_options(multi)
|
6
|
+
|
7
|
+
- multi.reports.each do |report|
|
8
|
+
= report.render layout: false
|
@@ -1,6 +1,8 @@
|
|
1
|
-
%
|
1
|
+
%h2= report.formatted_title
|
2
2
|
|
3
|
-
= link_to '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
|
data/config/routes.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Rails.application.routes.draw do
|
2
2
|
|
3
|
-
get "reports
|
4
|
-
get "multi/reports
|
3
|
+
get "reports/*report", to: 'dossier/reports#show', as: :dossier_report
|
4
|
+
get "multi/reports/*report", to: 'dossier/reports#multi', as: :dossier_multi_report
|
5
5
|
|
6
6
|
end
|
data/lib/dossier.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require "dossier/engine"
|
2
|
+
require "dossier/naming"
|
3
|
+
require "dossier/view_context_with_report_formatter"
|
2
4
|
require "dossier/version"
|
3
5
|
|
4
6
|
module Dossier
|
@@ -18,14 +20,6 @@ module Dossier
|
|
18
20
|
configuration.client
|
19
21
|
end
|
20
22
|
|
21
|
-
def class_to_name(klass)
|
22
|
-
klass.name.underscore[0..-8]
|
23
|
-
end
|
24
|
-
|
25
|
-
def name_to_class(name)
|
26
|
-
"#{name.split('_').map(&:capitalize).join}Report".constantize
|
27
|
-
end
|
28
|
-
|
29
23
|
class ExecuteError < StandardError; end
|
30
24
|
end
|
31
25
|
|
@@ -36,7 +30,9 @@ require "dossier/configuration"
|
|
36
30
|
require "dossier/formatter"
|
37
31
|
require "dossier/multi_report"
|
38
32
|
require "dossier/query"
|
33
|
+
require "dossier/renderer"
|
39
34
|
require "dossier/report"
|
35
|
+
require "dossier/responder"
|
40
36
|
require "dossier/result"
|
41
37
|
require "dossier/stream_csv"
|
42
38
|
require "dossier/xls"
|
@@ -15,7 +15,7 @@ module Dossier
|
|
15
15
|
|
16
16
|
def execute(query, report_name = nil)
|
17
17
|
# Ensure that SQL logs show name of report generating query
|
18
|
-
Result.new(connection.exec_query(*[query, report_name].compact))
|
18
|
+
Result.new(connection.exec_query(*["\n#{query}", report_name].compact))
|
19
19
|
rescue => e
|
20
20
|
raise Dossier::ExecuteError.new "#{e.message}\n\n#{query}"
|
21
21
|
end
|
data/lib/dossier/formatter.rb
CHANGED
@@ -1,18 +1,35 @@
|
|
1
1
|
module Dossier
|
2
2
|
module Formatter
|
3
|
+
include ActiveSupport::Inflector
|
4
|
+
include ActionView::Helpers::NumberHelper
|
3
5
|
extend self
|
4
|
-
extend ActiveSupport::Inflector
|
5
|
-
extend ActionView::Helpers::NumberHelper
|
6
6
|
|
7
7
|
def number_to_currency_from_cents(value)
|
8
8
|
number_to_currency(value /= 100.0)
|
9
9
|
end
|
10
10
|
|
11
|
+
def number_to_dollars(value)
|
12
|
+
"$#{commafy_number(value, 2)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def commafy_number(value, precision = nil)
|
16
|
+
whole, fraction = value.to_s.split('.')
|
17
|
+
fraction = "%.#{precision}d" % ("0.#{fraction}".to_f.round(precision) * 10**precision).to_i if precision
|
18
|
+
[whole.to_i.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,"), fraction].compact.join('.')
|
19
|
+
end
|
20
|
+
|
11
21
|
def url_formatter
|
12
22
|
@url_formatter ||= UrlFormatter.new
|
13
23
|
end
|
14
24
|
|
15
|
-
|
25
|
+
def report_name(report)
|
26
|
+
titleize("#{report.report_name.split('/').last} Report")
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO figure out how to handle this better
|
30
|
+
# reports rendered with a system layout use this link_to instead of the
|
31
|
+
# correct one
|
32
|
+
# delegate :url_for, :link_to, :url_helpers, to: :url_formatter
|
16
33
|
|
17
34
|
class UrlFormatter
|
18
35
|
include ActionView::Helpers::UrlHelper
|
data/lib/dossier/multi_report.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
class Dossier::MultiReport
|
2
|
+
include Dossier::Naming
|
2
3
|
|
3
4
|
attr_accessor :options
|
4
5
|
|
@@ -6,24 +7,31 @@ class Dossier::MultiReport
|
|
6
7
|
attr_accessor :reports
|
7
8
|
end
|
8
9
|
|
9
|
-
def self.report_name
|
10
|
-
Dossier.class_to_name(self)
|
11
|
-
end
|
12
|
-
|
13
10
|
def self.combine(*reports)
|
14
11
|
self.reports = reports
|
15
12
|
end
|
16
13
|
|
17
|
-
def self.report=(value)
|
18
|
-
value
|
19
|
-
end
|
20
|
-
|
21
14
|
def initialize(options = {})
|
22
15
|
self.options = options.dup.with_indifferent_access
|
23
16
|
end
|
24
17
|
|
25
18
|
def reports
|
26
|
-
@reports ||= self.class.reports.map
|
19
|
+
@reports ||= self.class.reports.map { |report|
|
20
|
+
report.new(options).tap { |r|
|
21
|
+
r.parent = self
|
22
|
+
}
|
23
|
+
}
|
27
24
|
end
|
28
25
|
|
26
|
+
def parent
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def formatter
|
31
|
+
Module.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def dom_id
|
35
|
+
nil
|
36
|
+
end
|
29
37
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Dossier
|
2
|
+
module Naming
|
3
|
+
|
4
|
+
# not using ActiveSupport::Concern because ClassMethods
|
5
|
+
# must be extended after ActiveModel::Naming
|
6
|
+
def self.included(base)
|
7
|
+
base.extend ActiveModel::Naming
|
8
|
+
base.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.class_to_name(klass)
|
12
|
+
(klass.name || anonymous_report).underscore[0..-8]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.name_to_class(name)
|
16
|
+
"#{name}_report".classify.constantize
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.anonymous_report
|
20
|
+
'AnonymousReport'
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_key
|
24
|
+
[report_name]
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
report_name
|
29
|
+
end
|
30
|
+
|
31
|
+
delegate :report_name, :formatted_title, to: "self.class"
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def report_name
|
35
|
+
Dossier::Naming.class_to_name(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def formatted_title
|
39
|
+
Dossier::Formatter.report_name(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def model_name
|
43
|
+
@model_name ||= ActiveModel::Name.new(self, nil, superclass.name).tap do |name|
|
44
|
+
name.instance_variable_set(:@param_key, 'options')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Dossier
|
2
|
+
class Renderer
|
3
|
+
attr_reader :report
|
4
|
+
attr_writer :engine
|
5
|
+
|
6
|
+
def initialize(report)
|
7
|
+
@report = report
|
8
|
+
end
|
9
|
+
|
10
|
+
def render(options = {})
|
11
|
+
render_template :custom, options
|
12
|
+
rescue ActionView::MissingTemplate => e
|
13
|
+
render_template :default, options
|
14
|
+
end
|
15
|
+
|
16
|
+
def engine
|
17
|
+
@engine ||= Engine.new(report)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def render_template(template, options)
|
23
|
+
template = send("#{template}_template_path")
|
24
|
+
engine.render options.merge(template: template, locals: {report: report})
|
25
|
+
end
|
26
|
+
|
27
|
+
def template_path(template)
|
28
|
+
"dossier/reports/#{template}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def custom_template_path
|
32
|
+
template_path(report.template)
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_template_path
|
36
|
+
template_path('show')
|
37
|
+
end
|
38
|
+
|
39
|
+
class Engine < AbstractController::Base
|
40
|
+
include AbstractController::Layouts
|
41
|
+
include ViewContextWithReportFormatter
|
42
|
+
attr_reader :report
|
43
|
+
|
44
|
+
layout 'dossier/layouts/application'
|
45
|
+
|
46
|
+
def self._helpers
|
47
|
+
Module.new do
|
48
|
+
include Rails.application.helpers
|
49
|
+
include Rails.application.routes_url_helpers
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self._view_paths
|
54
|
+
ActionController::Base.view_paths
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(report)
|
58
|
+
@report = report
|
59
|
+
super()
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/dossier/report.rb
CHANGED
@@ -1,16 +1,29 @@
|
|
1
1
|
module Dossier
|
2
2
|
class Report
|
3
|
+
include Dossier::Naming
|
3
4
|
include ActiveSupport::Callbacks
|
4
|
-
extend ActiveModel::Naming
|
5
5
|
|
6
6
|
define_callbacks :build_query, :execute
|
7
7
|
|
8
8
|
attr_reader :options
|
9
|
+
attr_accessor :parent
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
class_attribute :formatter
|
12
|
+
class_attribute :template
|
13
|
+
|
14
|
+
self.formatter = Dossier::Formatter
|
15
|
+
|
16
|
+
delegate :formatter, :template, to: "self.class"
|
17
|
+
|
18
|
+
def self.inherited(base)
|
19
|
+
super
|
20
|
+
base.template = base.report_name
|
12
21
|
end
|
13
22
|
|
23
|
+
def self.filename
|
24
|
+
"#{report_name.parameterize}-report_#{Time.now.strftime('%m-%d-%Y_%H-%M-%S')}"
|
25
|
+
end
|
26
|
+
|
14
27
|
def initialize(options = {})
|
15
28
|
@options = options.dup.with_indifferent_access
|
16
29
|
end
|
@@ -38,10 +51,6 @@ module Dossier
|
|
38
51
|
tap { execute }
|
39
52
|
end
|
40
53
|
|
41
|
-
def formatter
|
42
|
-
Dossier::Formatter
|
43
|
-
end
|
44
|
-
|
45
54
|
def format_header(header)
|
46
55
|
formatter.titleize(header.to_s)
|
47
56
|
end
|
@@ -50,6 +59,12 @@ module Dossier
|
|
50
59
|
Dossier.client
|
51
60
|
end
|
52
61
|
|
62
|
+
def renderer
|
63
|
+
@renderer ||= Renderer.new(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
delegate :render, to: :renderer
|
67
|
+
|
53
68
|
private
|
54
69
|
|
55
70
|
def build_query
|