compendium 0.0.1 → 1.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.
- checksums.yaml +15 -0
- data/README.md +70 -4
- data/Rakefile +4 -0
- data/app/assets/stylesheets/compendium/_metrics.css.scss +92 -0
- data/app/assets/stylesheets/compendium/options.css.scss +41 -0
- data/app/assets/stylesheets/compendium/report.css.scss +22 -0
- data/app/classes/compendium/presenters/base.rb +30 -0
- data/app/classes/compendium/presenters/chart.rb +31 -0
- data/app/classes/compendium/presenters/metric.rb +19 -0
- data/app/classes/compendium/presenters/option.rb +97 -0
- data/app/classes/compendium/presenters/query.rb +23 -0
- data/app/classes/compendium/presenters/settings/query.rb +22 -0
- data/app/classes/compendium/presenters/settings/table.rb +23 -0
- data/app/classes/compendium/presenters/table.rb +81 -0
- data/app/controllers/compendium/reports_controller.rb +52 -0
- data/app/helpers/compendium/reports_helper.rb +21 -0
- data/app/views/compendium/reports/run.haml +1 -0
- data/app/views/compendium/reports/setup.haml +14 -0
- data/compendium.gemspec +7 -1
- data/config/initializers/rails/active_record/connection_adapters/quoting.rb +14 -0
- data/config/initializers/ruby/numeric.rb +26 -0
- data/config/locales/en.yml +5 -0
- data/lib/compendium/abstract_chart_provider.rb +30 -0
- data/lib/compendium/chart_provider/amcharts.rb +20 -0
- data/lib/compendium/context_wrapper.rb +27 -0
- data/lib/compendium/dsl.rb +79 -0
- data/lib/compendium/engine/mount.rb +13 -0
- data/lib/compendium/engine.rb +8 -0
- data/lib/compendium/metric.rb +29 -0
- data/lib/compendium/open_hash.rb +68 -0
- data/lib/compendium/option.rb +37 -0
- data/lib/compendium/param_types.rb +91 -0
- data/lib/compendium/params.rb +40 -0
- data/lib/compendium/query.rb +94 -0
- data/lib/compendium/report.rb +56 -0
- data/lib/compendium/result_set.rb +24 -0
- data/lib/compendium/version.rb +1 -1
- data/lib/compendium.rb +46 -1
- data/spec/context_wrapper_spec.rb +71 -0
- data/spec/dsl_spec.rb +90 -0
- data/spec/metric_spec.rb +84 -0
- data/spec/option_spec.rb +12 -0
- data/spec/param_types_spec.rb +147 -0
- data/spec/params_spec.rb +28 -0
- data/spec/presenters/base_spec.rb +20 -0
- data/spec/presenters/option_spec.rb +49 -0
- data/spec/query_spec.rb +33 -0
- data/spec/report_spec.rb +93 -0
- data/spec/spec_helper.rb +1 -0
- metadata +135 -14
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MDMzZWY3N2Y4MWU5MGUxODg2YzFjZWY2ZDgzNzE0ZTU0MmNkN2IyYw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZDBjNTEwN2NjNGNlZWIxMzhiMWQ5MGRmMzg0NmY4NGQyNTlhYTAwNA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDQ1M2YwZDVjMWEyN2M2NjM4ZDYxOGY4ZTEwOTQ0M2Y0MWIzM2UxNDM2MGVm
|
10
|
+
YzIwMDFkYzM1ZjliNDUxMjBjNzQxNDkyMTIzYzlhNWQyNjViYTMwYmRhYmUy
|
11
|
+
ODBiODRhZjRkOGQ0MWVjNDdiNjE4MmE3MjM2OGNkNDZlZTE0NDk=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NjIzZGIwNWY1NTU3MzViOTU5Njc3MzFiNTdiYzdhZWViYjIwOGVlYzZmMDhm
|
14
|
+
NWI5NzkwNDJjYTUzYjIwNDg4ZjZkZDJhYzE5MDE0ZGQyM2U0Y2JlMGIzNThk
|
15
|
+
ODY0NDFkY2NhMDg1MWZkYmU3MjlmNjdhMzIxZWUzYWFjNjIyMzU=
|
data/README.md
CHANGED
@@ -2,6 +2,72 @@
|
|
2
2
|
|
3
3
|
Ruby on Rails framework for making reporting easy.
|
4
4
|
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Compendium is a reporting framework for Rails which makes it easy to create and render reports (with charts and tables).
|
8
|
+
|
9
|
+
A Compendium report is a subclass of `Compendium::Report`. Reports can be defined using the simple DSL:
|
10
|
+
|
11
|
+
class MyReport < Compendium::Report
|
12
|
+
# Options define which parameters your report will accept when being set up.
|
13
|
+
# An option is defined with a name, a type, and some settings (ie. default value, choices for radio buttons and
|
14
|
+
# dropdowns, etc.)
|
15
|
+
option :starting_on, :date, default: -> { Date.today - 1.month }
|
16
|
+
option :ending_on, :date, default: -> { Date.today }
|
17
|
+
option :currency, :radio, choices: [:USD, :CAD, :GBP]
|
18
|
+
|
19
|
+
# By default, queries are converted to SQL and executed instead of returning AR models
|
20
|
+
# The query definition block gets the report's current parameters
|
21
|
+
# totals: true means that the last row returned should be interpretted as a row of totals
|
22
|
+
query :deliveries, totals: true do |params|
|
23
|
+
Items.where(delivered: true, purchased_at: (params[:starting_on]..params[:ending_on]))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Define a query which collects data by using AR directly
|
27
|
+
query :on_hand_inventory, collect: :active_record do |params|
|
28
|
+
Items.where(in_stock: true)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Define a query that works on another query's result set
|
32
|
+
# Note: chart and data are aliases for query
|
33
|
+
chart :deliveries_over_time, through: :deliveries do |results|
|
34
|
+
results.group_by(&:purchased_at)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Queries can also be used to drive metrics
|
38
|
+
metric :shipping_time, -> results { results.last['shipping_time'] }, through: :deliveries
|
39
|
+
end
|
40
|
+
|
41
|
+
Reports can then also be simply instantiated (which is done automatically if using the supplied
|
42
|
+
`Compendium::ReportsController`):
|
43
|
+
|
44
|
+
report = MyReport.new(starting_on: '2013-06-01')
|
45
|
+
report.run(self) # The parameter is the context to run the report in; usually this should be
|
46
|
+
# a controller context so that methods like current_user can be used
|
47
|
+
|
48
|
+
Compendium also comes with a variety of different presenters, for rendering the setup page, and displaying charts
|
49
|
+
(`report.render_chart`), tables (`report.render_table`) and metrics for your report. Charting is delegated through a
|
50
|
+
`ChartProvider` to a charting gem (amcharts.rb is currently supported).
|
51
|
+
|
52
|
+
### Tying into your Rails application
|
53
|
+
|
54
|
+
Compendium has a `Rails::Engine`, which adds a default controller and some views. If desired, the controller can be
|
55
|
+
subclassed so that filters and the like can be added. The controller (which extends `ApplicationController`
|
56
|
+
automatically) has two actions: `setup` (collect options for the report) and `run` (execute and render the report),
|
57
|
+
with accompanying views. The `setup` view can be included inside your own view using the `render_report_setup`
|
58
|
+
method (*NOTE:* you have to pass `local_assigns` into it if you want locals to be passed along).
|
59
|
+
|
60
|
+
Routes are not automatically added to your application. In order to do so, you can use the `mount_compendium` helper
|
61
|
+
within your `config/routes.rb` file
|
62
|
+
|
63
|
+
mount_compendium at: '/report', controller: 'reports' # controller defaults to compendium/reports
|
64
|
+
|
65
|
+
### Interaction with other gems
|
66
|
+
* If [accessible_tooltip](https://github.com/dvandersluis/accessible_tooltip) is present, option notes will be rendered
|
67
|
+
in a tooltip rather than as straight text.
|
68
|
+
* [AmCharts.rb](https://github.com/dvandersluis/amcharts.rb) is currently the only chart provider (please create a pull
|
69
|
+
request if you'd like to create another one...)
|
70
|
+
|
5
71
|
## Installation
|
6
72
|
|
7
73
|
Add this line to your application's Gemfile:
|
@@ -16,10 +82,6 @@ Or install it yourself as:
|
|
16
82
|
|
17
83
|
$ gem install compendium
|
18
84
|
|
19
|
-
## Usage
|
20
|
-
|
21
|
-
TODO: Write usage instructions here
|
22
|
-
|
23
85
|
## Contributing
|
24
86
|
|
25
87
|
1. Fork it
|
@@ -27,3 +89,7 @@ TODO: Write usage instructions here
|
|
27
89
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
90
|
4. Push to the branch (`git push origin my-new-feature`)
|
29
91
|
5. Create new Pull Request
|
92
|
+
|
93
|
+
## Acknowledgments
|
94
|
+
|
95
|
+
* Special thanks to [TalentNest](http://github.com/talentnest), who sponsored this gem's development.
|
data/Rakefile
CHANGED
@@ -0,0 +1,92 @@
|
|
1
|
+
@import "compass/css3/border-radius";
|
2
|
+
|
3
|
+
div.metrics
|
4
|
+
{
|
5
|
+
div.metric
|
6
|
+
{
|
7
|
+
float: left;
|
8
|
+
margin: 10px;
|
9
|
+
padding: 5px;
|
10
|
+
border: 2px solid #575757;
|
11
|
+
width: 150px;
|
12
|
+
height: 150px;
|
13
|
+
font-size: 12px;
|
14
|
+
text-align: center;
|
15
|
+
color: #575757;
|
16
|
+
@include border-radius(10px);
|
17
|
+
|
18
|
+
.metric-label
|
19
|
+
{
|
20
|
+
height: 30%;
|
21
|
+
font-size: 150%;
|
22
|
+
padding-top: 10px;
|
23
|
+
padding-bottom: 5px;
|
24
|
+
font-weight: bold;
|
25
|
+
}
|
26
|
+
|
27
|
+
.metric-data
|
28
|
+
{
|
29
|
+
height: 50%;
|
30
|
+
position: relative;
|
31
|
+
font-size: 600%;
|
32
|
+
font-weight: bold;
|
33
|
+
color: black;
|
34
|
+
|
35
|
+
.metric-data-inner
|
36
|
+
{
|
37
|
+
display: inline;
|
38
|
+
position: relative;
|
39
|
+
line-height: 0;
|
40
|
+
top: 50%;
|
41
|
+
margin: auto;
|
42
|
+
padding: 0;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
.metric-label-small
|
47
|
+
{
|
48
|
+
height: 20%;
|
49
|
+
font_size: 90%;
|
50
|
+
}
|
51
|
+
|
52
|
+
&.small
|
53
|
+
{
|
54
|
+
width: 115px;
|
55
|
+
height: 115px;
|
56
|
+
font-size: 10px;
|
57
|
+
|
58
|
+
.metric-label
|
59
|
+
{
|
60
|
+
padding-top: 0px;
|
61
|
+
}
|
62
|
+
|
63
|
+
.metric-label-small
|
64
|
+
{
|
65
|
+
font-size: 100%;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
&.big
|
70
|
+
{
|
71
|
+
border-width: 3px;
|
72
|
+
width: 280px;
|
73
|
+
height: 280px;
|
74
|
+
font-size: 22px;
|
75
|
+
|
76
|
+
.metric-label
|
77
|
+
{
|
78
|
+
padding-top: 25px;
|
79
|
+
}
|
80
|
+
|
81
|
+
.metric-label-small
|
82
|
+
{
|
83
|
+
margin-top: -25px;
|
84
|
+
}
|
85
|
+
|
86
|
+
.metric-data
|
87
|
+
{
|
88
|
+
top: -10px;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
.option
|
2
|
+
{
|
3
|
+
.option-label
|
4
|
+
{
|
5
|
+
margin: 0px 0px 0px 0px;
|
6
|
+
font-family: "Arial";
|
7
|
+
font-size: 14px;
|
8
|
+
font-weight: bold;
|
9
|
+
color: #575757;
|
10
|
+
}
|
11
|
+
|
12
|
+
.option-note
|
13
|
+
{
|
14
|
+
font-style: italic;
|
15
|
+
color: #575757;
|
16
|
+
}
|
17
|
+
|
18
|
+
.option-element-group
|
19
|
+
{
|
20
|
+
margin-top: 3px;
|
21
|
+
}
|
22
|
+
|
23
|
+
.option-radio
|
24
|
+
{
|
25
|
+
input[type=radio]
|
26
|
+
{
|
27
|
+
margin: 2px 6px 2px 0;
|
28
|
+
|
29
|
+
& + label
|
30
|
+
{
|
31
|
+
position: relative;
|
32
|
+
top: 1px;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
& + &, & + input[type=submit]
|
38
|
+
{
|
39
|
+
margin-top: 15px;
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
.report
|
2
|
+
{
|
3
|
+
@import 'metrics';
|
4
|
+
|
5
|
+
.title
|
6
|
+
{
|
7
|
+
font-size: 200%;
|
8
|
+
font-weight: bold;
|
9
|
+
}
|
10
|
+
|
11
|
+
.subtitle
|
12
|
+
{
|
13
|
+
font-size: 150%;
|
14
|
+
font-weight: bold;
|
15
|
+
}
|
16
|
+
|
17
|
+
.setting, .option
|
18
|
+
{
|
19
|
+
font-weight: bold;
|
20
|
+
color: #575757;
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Compendium::Presenters
|
2
|
+
class Base
|
3
|
+
def initialize(template, object)
|
4
|
+
@object = object
|
5
|
+
@template = template
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
"#<#{self.class.name}:0x00#{'%x' % (object_id << 1)}>"
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.presents(name)
|
15
|
+
define_method(name) do
|
16
|
+
@object
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(*args, &block)
|
21
|
+
return @template.send(*args, &block) if @template.respond_to?(args.first)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to_missing?(*args)
|
26
|
+
return true if @template.respond_to?(*args)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Compendium::Presenters
|
2
|
+
class Chart < Query
|
3
|
+
attr_reader :data, :chart_provider
|
4
|
+
|
5
|
+
def initialize(template, object, type, container = nil, &setup)
|
6
|
+
super(template, object)
|
7
|
+
|
8
|
+
@data = results.records
|
9
|
+
@data = @data[0...-1] if query.options[:totals]
|
10
|
+
|
11
|
+
@container = container || query.name
|
12
|
+
|
13
|
+
initialize_chart_provider(type, &setup)
|
14
|
+
end
|
15
|
+
|
16
|
+
def render
|
17
|
+
chart_provider.render(@template, @container)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def provider
|
23
|
+
provider = Compendium.config.chart_provider
|
24
|
+
provider.is_a?(Class) ? provider : Compendium::ChartProvider.const_get(provider)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize_chart_provider(type, &setup)
|
28
|
+
@chart_provider = provider.new(type, @data, &setup)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Compendium::Presenters
|
2
|
+
class Metric < Base
|
3
|
+
presents :metric
|
4
|
+
|
5
|
+
delegate :name, :query, :ran?, to: :metric
|
6
|
+
|
7
|
+
def label
|
8
|
+
t("#{query}.#{name}")
|
9
|
+
end
|
10
|
+
|
11
|
+
def result(number_format = '%0.1f', display_nil_as = :na)
|
12
|
+
if metric.result
|
13
|
+
sprintf(number_format, metric.result)
|
14
|
+
else
|
15
|
+
t(display_nil_as)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Compendium::Presenters
|
2
|
+
class Option < Base
|
3
|
+
MISSING_CHOICES_ERROR = "choices must be specified"
|
4
|
+
|
5
|
+
presents :option
|
6
|
+
|
7
|
+
def name
|
8
|
+
t(option.name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def label(form)
|
12
|
+
label = case option.type.to_sym
|
13
|
+
when :boolean, :radio
|
14
|
+
name
|
15
|
+
|
16
|
+
else
|
17
|
+
form.label option.name, name
|
18
|
+
end
|
19
|
+
|
20
|
+
out = ActiveSupport::SafeBuffer.new
|
21
|
+
out << content_tag(:span, label, class: 'option-label')
|
22
|
+
|
23
|
+
if option.note?
|
24
|
+
note = t(option.note == true ? :"#{option.name}_note" : option.note)
|
25
|
+
|
26
|
+
if defined?(AccessibleTooltip)
|
27
|
+
return accessible_tooltip(:help, label: out, title: t("#{option.name}_note_title", default: '')) { note }
|
28
|
+
else
|
29
|
+
out << content_tag(:div, note, class: 'option-note')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
out
|
34
|
+
end
|
35
|
+
|
36
|
+
def note
|
37
|
+
if option.note?
|
38
|
+
key = option.note === true ? :"#{option.name}_note" : option.note
|
39
|
+
content_tag(:div, t(key), class: 'option-note')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def input(ctx, form)
|
44
|
+
out = ActiveSupport::SafeBuffer.new
|
45
|
+
|
46
|
+
case option.type.to_sym
|
47
|
+
when :date
|
48
|
+
out << date_field(form)
|
49
|
+
|
50
|
+
when :dropdown
|
51
|
+
raise ArgumentError, MISSING_CHOICES_ERROR unless option.choices?
|
52
|
+
|
53
|
+
options = option.choices
|
54
|
+
options = ctx.instance_exec(&options) if options.respond_to?(:call)
|
55
|
+
out << dropdown(form, options)
|
56
|
+
|
57
|
+
when :boolean, :radio
|
58
|
+
choices = if option.radio?
|
59
|
+
raise ArgumentError, MISSING_CHOICES_ERROR unless option.choices?
|
60
|
+
option.choices
|
61
|
+
else
|
62
|
+
%w(true false)
|
63
|
+
end
|
64
|
+
|
65
|
+
choices.each.with_index { |choice, index| out << radio_button(form, choice, index) }
|
66
|
+
end
|
67
|
+
|
68
|
+
out
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def date_field(form, include_time = false)
|
74
|
+
content_tag('div', class: 'option-date') do
|
75
|
+
if defined?(CalendarDateSelect)
|
76
|
+
form.calendar_date_select option.name, time: include_time, popup: 'force'
|
77
|
+
else
|
78
|
+
form.text_field option.name
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def dropdown(form, choices = {})
|
84
|
+
content_tag('div', class: 'option-dropdown') do
|
85
|
+
form.select option.name, choices
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def radio_button(form, label, value)
|
90
|
+
content_tag('div', class: 'option-radio') do
|
91
|
+
div_content = ActiveSupport::SafeBuffer.new
|
92
|
+
div_content << form.radio_button(option.name, value)
|
93
|
+
div_content << form.label(option.name, t(label), value: value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Compendium::Presenters
|
2
|
+
class Query < Base
|
3
|
+
presents :query
|
4
|
+
|
5
|
+
def initialize(template, object)
|
6
|
+
super(template, object)
|
7
|
+
end
|
8
|
+
|
9
|
+
def render
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def results
|
16
|
+
query.results
|
17
|
+
end
|
18
|
+
|
19
|
+
def settings_class
|
20
|
+
Settings.const_get(self.class.name.demodulize) rescue Settings::Query
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Compendium::Presenters::Settings
|
2
|
+
class Query
|
3
|
+
delegate :[], :fetch, to: :@settings
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@settings = {}.with_indifferent_access
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *args, &block)
|
10
|
+
if block_given?
|
11
|
+
@settings[name] = block.call(*args)
|
12
|
+
elsif !args.empty?
|
13
|
+
@settings[name] = args.length == 1 ? args.first : args
|
14
|
+
elsif name.to_s.end_with?('?')
|
15
|
+
prefix = name.to_s.gsub(/\?\z/, '')
|
16
|
+
@settings.key?(prefix)
|
17
|
+
else
|
18
|
+
@settings[name]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Compendium::Presenters::Settings
|
2
|
+
class Table < Query
|
3
|
+
attr_reader :headings
|
4
|
+
|
5
|
+
def initialize(headings)
|
6
|
+
super()
|
7
|
+
@headings = Hash[headings.zip(headings)].with_indifferent_access
|
8
|
+
end
|
9
|
+
|
10
|
+
def override_heading(col, label)
|
11
|
+
@headings[col] = label
|
12
|
+
end
|
13
|
+
|
14
|
+
def format(column, &block)
|
15
|
+
@settings[:formatters] ||= {}
|
16
|
+
@settings[:formatters][column] = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def formatters
|
20
|
+
(@settings[:formatters] || {})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Compendium::Presenters
|
2
|
+
class Table < Query
|
3
|
+
attr_reader :records, :totals
|
4
|
+
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
|
8
|
+
@records = results.records
|
9
|
+
@totals = @records.pop if has_totals_row?
|
10
|
+
|
11
|
+
@settings = settings_class.new(results.keys)
|
12
|
+
yield @settings if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
content_tag(:table, class: 'results') do
|
17
|
+
table = ActiveSupport::SafeBuffer.new
|
18
|
+
table << content_tag(:thead, build_heading_row)
|
19
|
+
table << content_tag(:tbody) do
|
20
|
+
tbody = ActiveSupport::SafeBuffer.new
|
21
|
+
records.each { |row| tbody << build_data_row(row) }
|
22
|
+
tbody
|
23
|
+
end
|
24
|
+
table << content_tag(:tfoot, build_totals_row) if has_totals_row?
|
25
|
+
table
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def headings
|
32
|
+
@settings.headings
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_totals_row?
|
36
|
+
query.options.fetch(:totals, false)
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_data_row(row)
|
40
|
+
build_row(row, 'data') { |key, val| formatted_value(key, val) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_heading_row
|
44
|
+
build_row(headings, 'headings', :th) { |key, val| t(val) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_totals_row
|
48
|
+
totals[totals.keys.first] = t(:total)
|
49
|
+
build_row(totals, 'totals', :th) { |key, val| formatted_value(key, val) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_row(row, row_class, cell_type = :td)
|
53
|
+
content_tag('tr', class: row_class) do
|
54
|
+
out = ActiveSupport::SafeBuffer.new
|
55
|
+
|
56
|
+
row.each.with_index do |(key, val), i|
|
57
|
+
val = yield key, val, i if block_given?
|
58
|
+
out << content_tag(cell_type, val)
|
59
|
+
end
|
60
|
+
|
61
|
+
out
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def formatted_value(k, v)
|
66
|
+
if @settings.formatters[k]
|
67
|
+
@settings.formatters[k].call(v)
|
68
|
+
else
|
69
|
+
if v.numeric?
|
70
|
+
if v.zero? and @settings.display_zero_as?
|
71
|
+
@settings.display_zero_as
|
72
|
+
else
|
73
|
+
sprintf(@settings.number_format || '%0.2f', v)
|
74
|
+
end
|
75
|
+
elsif v.nil?
|
76
|
+
@settings.display_nil_as
|
77
|
+
end
|
78
|
+
end || v
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Compendium
|
2
|
+
class ReportsController < ::ApplicationController
|
3
|
+
helper Compendium::ReportsHelper
|
4
|
+
|
5
|
+
before_filter :find_report
|
6
|
+
before_filter :run_report, only: :run
|
7
|
+
|
8
|
+
def setup
|
9
|
+
render locals: { report: setup_report, prefix: @prefix }
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
template = template_exists?(@prefix, get_template_prefixes) ? @prefix : 'run'
|
14
|
+
render action: template, locals: { report: @report }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def find_report
|
20
|
+
@prefix = params[:report_name]
|
21
|
+
@report_name = "#{@prefix}_report"
|
22
|
+
|
23
|
+
begin
|
24
|
+
require(@report_name) unless Rails.env.development? or Module.const_defined?(@report_name.classify)
|
25
|
+
@report_class = @report_name.camelize.constantize
|
26
|
+
rescue LoadError
|
27
|
+
flash[:error] = t(:invalid_report)
|
28
|
+
redirect_to action: :index
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def setup_report
|
33
|
+
@report_class.new(params[:report] || {})
|
34
|
+
end
|
35
|
+
|
36
|
+
def run_report
|
37
|
+
@report = @report_class.new(params[:report]).run(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_template_prefixes
|
41
|
+
paths = []
|
42
|
+
klass = self.class
|
43
|
+
|
44
|
+
begin
|
45
|
+
paths << klass.name.underscore.gsub(/_controller$/, '')
|
46
|
+
klass = klass.superclass
|
47
|
+
end while(klass != ActionController::Base)
|
48
|
+
|
49
|
+
paths
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Compendium
|
2
|
+
module ReportsHelper
|
3
|
+
def expose(*args)
|
4
|
+
klass = args.pop if args.last.is_a?(Class)
|
5
|
+
klass ||= "Compendium::Presenters::#{args.first.class}".constantize
|
6
|
+
presenter = klass.new(self, *(args.empty? ? [nil] : args))
|
7
|
+
yield presenter if block_given?
|
8
|
+
presenter
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_report_setup(assigns)
|
12
|
+
render file: "#{Compendium::Engine.root}/app/views/compendium/reports/setup", locals: assigns
|
13
|
+
end
|
14
|
+
|
15
|
+
def render_if_exists(options = {})
|
16
|
+
if lookup_context.template_exists?(options[:partial] || options[:template], options[:path], options.key?(:partial))
|
17
|
+
render options
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
= t(:results, report_name: @prefix)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
- content_for :stylesheets do
|
2
|
+
= stylesheet_link_tag 'compendium/options'
|
3
|
+
|
4
|
+
= render_if_exists partial: 'report_header', path: 'compendium/reports'
|
5
|
+
|
6
|
+
.options
|
7
|
+
= form_for report, as: :report, url: compendium_reports_run_path do |f|
|
8
|
+
- report.options.values.each do |option|
|
9
|
+
- expose option, Compendium::Presenters::Option do |opt|
|
10
|
+
.option
|
11
|
+
= opt.label(f)
|
12
|
+
.option-element-group= opt.input(self, f)
|
13
|
+
|
14
|
+
= f.submit t(:generate_report, scope: 'compendium.reports')
|