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
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
|