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
data/compendium.gemspec
CHANGED
@@ -10,12 +10,18 @@ Gem::Specification.new do |gem|
|
|
10
10
|
gem.email = ["dvandersluis@selfmgmt.com"]
|
11
11
|
gem.description = %q{Ruby on Rails reporting framework}
|
12
12
|
gem.summary = %q{Ruby on Rails reporting framework}
|
13
|
-
gem.homepage = ""
|
13
|
+
gem.homepage = "https://github.com/dvandersluis/compendium"
|
14
|
+
gem.license = "MIT"
|
14
15
|
|
15
16
|
gem.files = `git ls-files`.split($/)
|
16
17
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
18
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
19
|
gem.require_paths = ["lib"]
|
19
20
|
|
21
|
+
gem.add_dependency 'rails', '>= 3.0.0'
|
22
|
+
gem.add_dependency 'sass-rails', '>= 3.0.0'
|
23
|
+
gem.add_dependency 'compass-rails', '>= 1.0.0'
|
24
|
+
gem.add_dependency 'collection_of', '>= 1.0.3'
|
25
|
+
gem.add_dependency 'inheritable_attr', '>= 1.0.0'
|
20
26
|
gem.add_development_dependency 'rspec', '~> 2.0'
|
21
27
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# ActiveRecord doesn't know how to handle SimpleDelegators when creating SQL
|
2
|
+
# This means that when passing a SimpleDelegator (ie. Compendium::Param) into ActiveRecord::Base.find, it'll
|
3
|
+
# crash.
|
4
|
+
# Override AR::ConnectionAdapters::Quoting to forward a SimpleDelegator's object to be quoted.
|
5
|
+
|
6
|
+
module ActiveRecord::ConnectionAdapters::Quoting
|
7
|
+
def quote_with_simple_delegator(value, column = nil)
|
8
|
+
return value.quoted_id if value.respond_to?(:quoted_id)
|
9
|
+
value = value.__getobj__ if value.is_a?(SimpleDelegator)
|
10
|
+
quote_without_simple_delegator(value, column)
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method_chain :quote, :simple_delegator
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Monkey patch to determine if an object is numeric
|
2
|
+
# Only Numerics and Strings/Symbols that are representations of numbers are numeric
|
3
|
+
|
4
|
+
class Object
|
5
|
+
def numeric?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class String
|
11
|
+
def numeric?
|
12
|
+
!(self =~ /\A-?\d+(\.\d+)?\z|\A-?\.\d+\z/).nil?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Symbol
|
17
|
+
def numeric?
|
18
|
+
to_s.numeric?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Numeric
|
23
|
+
def numeric?
|
24
|
+
true
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
|
3
|
+
module Compendium
|
4
|
+
# Abstract wrapper for rendering charts
|
5
|
+
# To add a new chart provider, #initialize and #render must be implemented
|
6
|
+
class AbstractChartProvider
|
7
|
+
attr_reader :chart
|
8
|
+
|
9
|
+
def initialize(type, data, &setup_proc)
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
def render(template, container)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# As more chart providers are added, this method will have to be extended to find them
|
18
|
+
def self.find_chart_provider
|
19
|
+
if defined?(AmCharts)
|
20
|
+
:AmCharts
|
21
|
+
else
|
22
|
+
self.name.demodulize.to_sym
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ChartProvider
|
28
|
+
autoload :AmCharts, 'compendium/chart_provider/amcharts'
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Compendium
|
2
|
+
module ChartProvider
|
3
|
+
# Uses the amcharts.rb gem to provide charting
|
4
|
+
class AmCharts < Compendium::AbstractChartProvider
|
5
|
+
def initialize(type, data, &setup_proc)
|
6
|
+
@chart = chart_class(type).new(data, &setup_proc)
|
7
|
+
end
|
8
|
+
|
9
|
+
def render(template, container)
|
10
|
+
template.amchart(chart, container)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def chart_class(type)
|
16
|
+
::AmCharts::Chart.const_get(type.to_s.titlecase)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Compendium
|
4
|
+
class ContextWrapper
|
5
|
+
def self.wrap(ctx, parent, params = nil, &block)
|
6
|
+
delegator = ::SimpleDelegator.new(parent)
|
7
|
+
|
8
|
+
delegator.define_singleton_method(:__context__) { ctx }
|
9
|
+
|
10
|
+
delegator.instance_eval do
|
11
|
+
def method_missing(name, *args, &block)
|
12
|
+
return __context__.__send__(name, *args, &block) if __context__.respond_to?(name)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def respond_to_missing?(name, include_private = false)
|
17
|
+
return true if __context__.respond_to?(name, include_private)
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
return delegator.instance_exec(params, &block) if block_given?
|
23
|
+
|
24
|
+
delegator
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'collection_of'
|
2
|
+
require 'inheritable_attr'
|
3
|
+
require 'compendium/option'
|
4
|
+
require 'active_support/core_ext/class/attribute'
|
5
|
+
|
6
|
+
module Compendium
|
7
|
+
module DSL
|
8
|
+
def self.extended(klass)
|
9
|
+
klass.inheritable_attr :queries, default: ::Collection[Query]
|
10
|
+
klass.inheritable_attr :options, default: {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def query(name, opts = {}, &block)
|
14
|
+
define_query(name, opts, &block)
|
15
|
+
end
|
16
|
+
alias_method :chart, :query
|
17
|
+
alias_method :data, :query
|
18
|
+
|
19
|
+
def option(name, *args)
|
20
|
+
opts = args.extract_options!
|
21
|
+
type = args.shift
|
22
|
+
|
23
|
+
if options[name]
|
24
|
+
options[name].type = type if type
|
25
|
+
options[name].merge!(opts)
|
26
|
+
else
|
27
|
+
options[name] = Compendium::Option.new(opts.merge(name: name, type: type))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def metric(name, proc, opts = {})
|
32
|
+
raise ArgumentError, 'through option must be specified for metric' unless opts.key?(:through)
|
33
|
+
|
34
|
+
[opts.delete(:through)].flatten.each do |query|
|
35
|
+
raise ArgumentError, "query #{query} is not defined" unless queries.key?(query)
|
36
|
+
queries[query].add_metric(name, proc, opts)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Allow defined queries to be redefined by name, eg:
|
41
|
+
# query :main_query
|
42
|
+
# main_query { collect_records_here }
|
43
|
+
def method_missing(name, *args, &block)
|
44
|
+
if queries.keys.include?(name.to_sym)
|
45
|
+
query = queries[name.to_sym]
|
46
|
+
query.proc = block if block_given?
|
47
|
+
query.options = args.extract_options!
|
48
|
+
return query
|
49
|
+
end
|
50
|
+
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def respond_to_missing?(name, *args)
|
55
|
+
return true if queries.keys.include?(name)
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def define_query(name, opts, type = Query, &block)
|
62
|
+
name = name.to_sym
|
63
|
+
query = type.new(name, opts, block)
|
64
|
+
|
65
|
+
if opts.key?(:through)
|
66
|
+
through = [opts[:through]].flatten
|
67
|
+
|
68
|
+
through.each do |q|
|
69
|
+
raise ArgumentError, "query #{q} is not defined" unless self.queries.include?(q.to_sym)
|
70
|
+
end
|
71
|
+
|
72
|
+
query.through = through
|
73
|
+
end
|
74
|
+
|
75
|
+
metrics[name] = opts[:metric] if opts.key?(:metric)
|
76
|
+
queries << query
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActionDispatch
|
2
|
+
module Routing
|
3
|
+
class Mapper
|
4
|
+
def mount_compendium(options = {})
|
5
|
+
scope options[:at], controller: options.fetch(:controller, 'compendium/reports') do
|
6
|
+
get ':report_name', action: :setup, as: 'compendium_reports_setup'
|
7
|
+
post ':report_name', action: :run, as: 'compendium_reports_run'
|
8
|
+
root action: :index, as: 'compendium_reports_root'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Compendium
|
2
|
+
Metric = Struct.new(:name, :query, :command, :options) do
|
3
|
+
attr_accessor :result
|
4
|
+
|
5
|
+
def initialize(*)
|
6
|
+
super
|
7
|
+
self.options ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(ctx, data)
|
11
|
+
self.result = if condition_failed?(ctx)
|
12
|
+
nil
|
13
|
+
else
|
14
|
+
command.is_a?(Symbol) ? ctx.send(command, data) : ctx.instance_exec(data, &command)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def ran?
|
19
|
+
!result.nil?
|
20
|
+
end
|
21
|
+
alias_method :has_ran?, :ran?
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def condition_failed?(ctx)
|
26
|
+
(options.key?(:if) and !ctx.instance_exec(&options[:if])) or (options.key?(:unless) and ctx.instance_exec(&options[:unless]))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
|
3
|
+
module Compendium
|
4
|
+
class OpenHash < ::ActiveSupport::HashWithIndifferentAccess
|
5
|
+
class << self
|
6
|
+
def [](hash = {})
|
7
|
+
new(hash)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def dup
|
12
|
+
self.class.new(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def convert_value(value)
|
18
|
+
if value.is_a? Hash
|
19
|
+
Params[value].tap do |oh|
|
20
|
+
oh.each do |k, v|
|
21
|
+
oh[k] = convert_value(v) if v.is_a? Hash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
elsif value.is_a?(Array)
|
25
|
+
value.dup.replace(value.map { |e| convert_value(e) })
|
26
|
+
else
|
27
|
+
value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(name, *args, &block)
|
32
|
+
method = name.to_s
|
33
|
+
|
34
|
+
case method
|
35
|
+
when %r{.=$}
|
36
|
+
super unless args.length == 1
|
37
|
+
return self[method[0...-1]] = args.first
|
38
|
+
|
39
|
+
when %r{.\?$}
|
40
|
+
super unless args.empty?
|
41
|
+
return self.key?(method[0...-1].to_sym)
|
42
|
+
|
43
|
+
when %r{^_.}
|
44
|
+
super unless args.empty?
|
45
|
+
return self[method[1..-1]] if self.key?(method[1..-1].to_sym)
|
46
|
+
|
47
|
+
else
|
48
|
+
return self[method] if key?(method) or !respond_to?(method)
|
49
|
+
end
|
50
|
+
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def respond_to_missing?(name, include_private = false)
|
55
|
+
method = name.to_s
|
56
|
+
|
57
|
+
case method
|
58
|
+
when %r{.[=?]$}
|
59
|
+
return true if self.key?(method[0...-1])
|
60
|
+
|
61
|
+
when %r{^_.}
|
62
|
+
return true if self.key?(method[1..-1])
|
63
|
+
end
|
64
|
+
|
65
|
+
super
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'compendium/open_hash'
|
2
|
+
require 'active_support/string_inquirer'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
6
|
+
module Compendium
|
7
|
+
class Option
|
8
|
+
attr_reader :name, :type, :default, :options
|
9
|
+
|
10
|
+
delegate :boolean?, :date?, :dropdown?, :radio?, :text?, to: :type
|
11
|
+
delegate :merge, :merge!, :[], to: :@options
|
12
|
+
|
13
|
+
def initialize(hash = {})
|
14
|
+
raise ArgumentError, "name must be provided" unless hash.key?(:name)
|
15
|
+
|
16
|
+
@name = hash.delete(:name).to_sym
|
17
|
+
@default = hash.delete(:default)
|
18
|
+
self.type = hash.delete(:type)
|
19
|
+
@options = hash.with_indifferent_access
|
20
|
+
end
|
21
|
+
|
22
|
+
def type=(type)
|
23
|
+
@type = ActiveSupport::StringInquirer.new(type.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(name, *args, &block)
|
27
|
+
return options[name] if options.key?(name)
|
28
|
+
return options.key?(name[0...-1]) if name.to_s.end_with?('?')
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to_missing?(name, include_private = false)
|
33
|
+
return true if options.key?(name)
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative '../../config/initializers/ruby/numeric'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module Compendium
|
5
|
+
class Param < ::SimpleDelegator
|
6
|
+
def boolean?; false; end
|
7
|
+
def date?; false; end
|
8
|
+
def dropdown?; false; end
|
9
|
+
def radio?; false; end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
return true if (value == other rescue false)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
# Need to explicitly delegate nil? to the object, otherwise it's always false
|
17
|
+
# This is because SimpleDelegator is a non-nil object, and it only forwards non-defined methods!
|
18
|
+
def nil?
|
19
|
+
__getobj__.nil?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ParamWithChoices < Param
|
24
|
+
def initialize(obj, choices)
|
25
|
+
@choices = choices
|
26
|
+
|
27
|
+
if @choices.respond_to?(:call)
|
28
|
+
# If given a proc, defer determining values until later.
|
29
|
+
index = obj
|
30
|
+
else
|
31
|
+
index = obj.numeric? ? obj.to_i : @choices.index(obj)
|
32
|
+
raise IndexError if (!obj.nil? and index.nil?) or index.to_i.abs > @choices.length - 1
|
33
|
+
end
|
34
|
+
|
35
|
+
super(index || 0)
|
36
|
+
end
|
37
|
+
|
38
|
+
def value
|
39
|
+
@choices[self]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class BooleanParam < Param
|
44
|
+
def initialize(obj, *)
|
45
|
+
# If given 0, 1, or a version thereof (ie. "0"), pass it along
|
46
|
+
return super obj.to_i if obj.numeric? and (0..1).cover?(obj.to_i)
|
47
|
+
super !!obj ? 0 : 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def boolean?
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def value
|
55
|
+
[true, false][self]
|
56
|
+
end
|
57
|
+
|
58
|
+
# When negating a BooleanParam, use the value instead
|
59
|
+
def !
|
60
|
+
!value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class DateParam < Param
|
65
|
+
def initialize(obj, *)
|
66
|
+
if obj.respond_to?(:to_date)
|
67
|
+
obj = obj.to_date
|
68
|
+
else
|
69
|
+
obj = Date.parse(obj) rescue nil
|
70
|
+
end
|
71
|
+
|
72
|
+
super obj
|
73
|
+
end
|
74
|
+
|
75
|
+
def date?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class RadioParam < ParamWithChoices
|
81
|
+
def radio?
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class DropdownParam < ParamWithChoices
|
87
|
+
def dropdown?
|
88
|
+
true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'compendium/open_hash'
|
2
|
+
require 'compendium/param_types'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'active_support/core_ext/object/blank'
|
5
|
+
|
6
|
+
module Compendium
|
7
|
+
class Params < OpenHash
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
def initialize(hash = {}, options = {})
|
11
|
+
@options = options
|
12
|
+
super(prepare_hash_from_options(hash))
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def prepare_hash_from_options(params)
|
18
|
+
params = params.slice(*options.keys)
|
19
|
+
|
20
|
+
options.each do |option_name, metadata|
|
21
|
+
begin
|
22
|
+
klass = "Compendium::#{"#{metadata.type}Param".classify}".constantize
|
23
|
+
params[option_name] = klass.new(get_default_value(params[option_name], metadata.default), metadata[:choices])
|
24
|
+
rescue IndexError
|
25
|
+
raise IndexError, "invalid index for #{option_name}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
params
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_default_value(current, default)
|
33
|
+
if current.blank? and !default.blank?
|
34
|
+
default.respond_to?(:call) ? default.call : default
|
35
|
+
else
|
36
|
+
current
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'compendium/result_set'
|
2
|
+
require 'compendium/params'
|
3
|
+
require 'collection_of'
|
4
|
+
|
5
|
+
module Compendium
|
6
|
+
class Query
|
7
|
+
attr_reader :name, :results, :metrics
|
8
|
+
attr_accessor :options, :proc, :through, :report
|
9
|
+
|
10
|
+
def initialize(name, options, proc)
|
11
|
+
@name = name
|
12
|
+
@options = options
|
13
|
+
@proc = proc
|
14
|
+
@metrics = ::Collection[Metric]
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize_clone(*)
|
18
|
+
super
|
19
|
+
@metrics = @metrics.clone
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(params, context = self)
|
23
|
+
collect_results(params, context)
|
24
|
+
collect_metrics(context)
|
25
|
+
|
26
|
+
@results
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_metric(name, proc, options = {})
|
30
|
+
Compendium::Metric.new(name, self.name, proc, options).tap { |m| @metrics << m }
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_table(template, *options, &block)
|
34
|
+
Compendium::Presenters::Table.new(template, self, *options, &block).render
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_chart(template, *options, &block)
|
38
|
+
Compendium::Presenters::Chart.new(template, self, *options, &block).render
|
39
|
+
end
|
40
|
+
|
41
|
+
def ran?
|
42
|
+
!@results.nil?
|
43
|
+
end
|
44
|
+
alias_method :has_run?, :ran?
|
45
|
+
|
46
|
+
def nil?
|
47
|
+
proc.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def collect_results(params, context)
|
53
|
+
args = if through.nil?
|
54
|
+
params
|
55
|
+
else
|
56
|
+
collect_through_query_results(through, params, context)
|
57
|
+
end
|
58
|
+
|
59
|
+
command = context.instance_exec(args, &proc) if proc
|
60
|
+
command = fetch_results(command)
|
61
|
+
@results = ResultSet.new(command) if command
|
62
|
+
end
|
63
|
+
|
64
|
+
def collect_metrics(context)
|
65
|
+
metrics.each{ |m| m.run(context, results) } unless results.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch_results(command)
|
69
|
+
if options.key?(:through) or options.fetch(:collect, nil) == :active_record
|
70
|
+
command
|
71
|
+
else
|
72
|
+
::ActiveRecord::Base.connection.select_all(command.respond_to?(:to_sql) ? command.to_sql : command)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def collect_through_query_results(through, params, context)
|
77
|
+
results = {}
|
78
|
+
|
79
|
+
through = [through].flatten.map(&method(:get_through_query))
|
80
|
+
|
81
|
+
through.each do |q|
|
82
|
+
q.run(params, context) unless q.ran?
|
83
|
+
results[q.name] = q.results.records
|
84
|
+
end
|
85
|
+
|
86
|
+
results = results[through.first.name] if through.size == 1
|
87
|
+
results
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_through_query(name)
|
91
|
+
report.queries[name]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
2
|
+
require 'active_support/core_ext/hash/slice'
|
3
|
+
require 'compendium/dsl'
|
4
|
+
|
5
|
+
module Compendium
|
6
|
+
class Report
|
7
|
+
attr_accessor :params, :results
|
8
|
+
|
9
|
+
extend Compendium::DSL
|
10
|
+
|
11
|
+
def self.inherited(report)
|
12
|
+
Compendium.reports << report
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(params = {})
|
16
|
+
@params = Params.new(params, options)
|
17
|
+
|
18
|
+
# When creating a new report, map each query back to the report
|
19
|
+
queries.each { |q| q.report = self }
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(context = nil)
|
23
|
+
self.context = context
|
24
|
+
self.results = {}
|
25
|
+
|
26
|
+
queries.each{ |q| self.results[q.name] = q.run(params, ContextWrapper.wrap(context, self)) }
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def metrics
|
32
|
+
Collection[Metric, queries.map{ |q| q.metrics.to_a }.flatten]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :context
|
38
|
+
|
39
|
+
def method_missing(name, *args, &block)
|
40
|
+
prefix = name.to_s.sub(/(?:_results|\?)\Z/, '').to_sym
|
41
|
+
|
42
|
+
return queries[name] if queries.keys.include?(name)
|
43
|
+
return results[prefix] if name.to_s.end_with? '_results' and queries.keys.include?(prefix)
|
44
|
+
return params[name] if options.keys.include?(name)
|
45
|
+
return !!params[prefix] if name.to_s.end_with? '?' and options.keys.include?(prefix)
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_to_missing?(name, include_private = false)
|
50
|
+
prefix = name.to_s.sub(/_results\Z/, '').to_sym
|
51
|
+
return true if queries.keys.include?(name)
|
52
|
+
return true if name.to_s.end_with? '_results' and queries.keys.include?(prefix)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
3
|
+
module Compendium
|
4
|
+
class ResultSet
|
5
|
+
delegate :first, :last, :to_a, :empty?, :each, :map, :[], :count, :length, :size, :==, to: :records
|
6
|
+
|
7
|
+
attr_reader :records
|
8
|
+
alias :all :records
|
9
|
+
|
10
|
+
def initialize(records)
|
11
|
+
@records = records.map do |r|
|
12
|
+
r.respond_to?(:with_indifferent_access) ? r.with_indifferent_access : r
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def keys
|
17
|
+
records.is_a?(Array) ? first.keys : records.keys
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_json(options = {})
|
21
|
+
records.map{ |r| r.except(*options[:except]) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|