compendium 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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')
|