reporter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/MIT-LICENSE +21 -0
- data/README.markdown +310 -0
- data/Rakefile +62 -0
- data/VERSION +1 -0
- data/lib/reporter/data_set.rb +53 -0
- data/lib/reporter/data_source/active_record_source.rb +136 -0
- data/lib/reporter/data_source/scoping.rb +153 -0
- data/lib/reporter/data_source.rb +30 -0
- data/lib/reporter/data_structure.rb +41 -0
- data/lib/reporter/field/average_field.rb +7 -0
- data/lib/reporter/field/base.rb +21 -0
- data/lib/reporter/field/calculation_field.rb +32 -0
- data/lib/reporter/field/count_field.rb +9 -0
- data/lib/reporter/field/field.rb +25 -0
- data/lib/reporter/field/formula_field.rb +24 -0
- data/lib/reporter/field/sum_field.rb +7 -0
- data/lib/reporter/formula.rb +371 -0
- data/lib/reporter/result_row.rb +37 -0
- data/lib/reporter/scope/base.rb +37 -0
- data/lib/reporter/scope/date_scope.rb +109 -0
- data/lib/reporter/scope/reference_scope.rb +154 -0
- data/lib/reporter/support/time_range.rb +62 -0
- data/lib/reporter/time_iterator.rb +85 -0
- data/lib/reporter/time_optimized_result_row.rb +39 -0
- data/lib/reporter/value.rb +36 -0
- metadata +139 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
# The scoping class manages the active scopes applied on all datasources.
|
2
|
+
# It takes care of serialization of the scopes when they are switched between iterations of the data.
|
3
|
+
#
|
4
|
+
class Reporter::DataSource::Scoping
|
5
|
+
|
6
|
+
SUPPORTED_SCOPES = {
|
7
|
+
:date => Reporter::Scope::DateScope,
|
8
|
+
:reference => Reporter::Scope::ReferenceScope
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize data_source
|
12
|
+
@data_source = data_source
|
13
|
+
@defined_scopes = {}
|
14
|
+
@scope_serialization = {}
|
15
|
+
@scope_hash = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
public
|
19
|
+
|
20
|
+
# returns an array with all possible scopes between all datasources
|
21
|
+
def possible
|
22
|
+
return @possible_scopes if @possible_scopes
|
23
|
+
results = SUPPORTED_SCOPES.collect { |type_name, scope_type| scope_type.possible_scopes data_source.sources }.flatten
|
24
|
+
#[{ :funeral => "work_area", :cbs_statistic => "work_area" }, { :funeral => "notification_date", :cbs_statistic => "month" }]
|
25
|
+
@possible_scopes = results.uniq
|
26
|
+
end
|
27
|
+
|
28
|
+
def << scope_type, name, *args
|
29
|
+
raise "Invalid scope #{scope_type}" unless SUPPORTED_SCOPES.keys.include? scope_type
|
30
|
+
@defined_scopes[name] = SUPPORTED_SCOPES[scope_type].new(self, name, data_source, *args)
|
31
|
+
#Rails.logger.info "Added scope #{name}: #{scope_type}"
|
32
|
+
self
|
33
|
+
end
|
34
|
+
alias :add :<<
|
35
|
+
|
36
|
+
def limit_scope scope, *args
|
37
|
+
get(scope).limit = *args
|
38
|
+
end
|
39
|
+
|
40
|
+
def change changes
|
41
|
+
changes.each do |scope, change|
|
42
|
+
get(scope).change change
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def get scope
|
48
|
+
raise "Scope does not exist" unless @defined_scopes.has_key? scope
|
49
|
+
@defined_scopes[scope]
|
50
|
+
end
|
51
|
+
|
52
|
+
# internal
|
53
|
+
|
54
|
+
def apply_on source, options
|
55
|
+
ignored_scopes = options[:ignore_scopes] || []
|
56
|
+
c = @cached_scopes[source][ignored_scopes.hash][@scope_hash]
|
57
|
+
raise "create" if c.nil?
|
58
|
+
# Rails.logger.info "cache!"
|
59
|
+
c
|
60
|
+
rescue
|
61
|
+
# Rails.logger.info "scope creation!"
|
62
|
+
c_source = source
|
63
|
+
@cached_scopes ||= {}
|
64
|
+
@cached_scopes[c_source] ||= {}
|
65
|
+
@cached_scopes[c_source][ignored_scopes.hash] ||= {}
|
66
|
+
@defined_scopes.each do |name, scope|
|
67
|
+
source = scope.apply_on(source) unless ignored_scopes.include? name
|
68
|
+
end
|
69
|
+
@cached_scopes[c_source][ignored_scopes.hash][@scope_hash] = source
|
70
|
+
end
|
71
|
+
|
72
|
+
def normalize_mapping mapping
|
73
|
+
normalized = {}
|
74
|
+
mapping.each do |key, value|
|
75
|
+
key_s = key.to_s
|
76
|
+
source = data_source.sources.find { |source| source.model_name.underscore == key_s.underscore }
|
77
|
+
normalized_value = \
|
78
|
+
case value
|
79
|
+
when Array :
|
80
|
+
value.collect(&:to_s)
|
81
|
+
when Hash :
|
82
|
+
value
|
83
|
+
else
|
84
|
+
value.to_s
|
85
|
+
end
|
86
|
+
normalized[source.model_name] = normalized_value
|
87
|
+
end
|
88
|
+
normalized.freeze
|
89
|
+
end
|
90
|
+
|
91
|
+
def valid_scope? mapping
|
92
|
+
!(get_scope_type mapping == false)
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_scope_type mapping
|
96
|
+
if mapping.is_a? Hash
|
97
|
+
mapping = normalize_mapping mapping
|
98
|
+
#Rails.logger.info mapping.inspect
|
99
|
+
valid = false
|
100
|
+
possible.each do |fields|
|
101
|
+
if (mapping.keys & fields.keys) == mapping.keys
|
102
|
+
combination_valid = true
|
103
|
+
mapping.each do |source, column|
|
104
|
+
columns = fields[source].is_a?(Array) ? fields[source] : [fields[source]]
|
105
|
+
#Rails.logger.info "#{columns.inspect} <=> #{column.inspect}"
|
106
|
+
combination_valid = false unless columns.include? column
|
107
|
+
end
|
108
|
+
return fields[:type] if combination_valid
|
109
|
+
end
|
110
|
+
end
|
111
|
+
elsif mapping.ancestors.include? ActiveRecord::Base
|
112
|
+
return :reference
|
113
|
+
end
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
# internal serialization
|
118
|
+
|
119
|
+
def current_scope
|
120
|
+
scope_serialization
|
121
|
+
end
|
122
|
+
|
123
|
+
def apply_scope scope_serialization
|
124
|
+
@scope_serialization = scope_serialization
|
125
|
+
@scope_hash = scope_serialization.hash
|
126
|
+
end
|
127
|
+
|
128
|
+
def serialize_scope(scope_name, value)
|
129
|
+
scope_serialization[scope_name] = value
|
130
|
+
@scope_hash = scope_serialization.hash
|
131
|
+
end
|
132
|
+
|
133
|
+
def unserialize_scope(scope_name)
|
134
|
+
scope_serialization[scope_name]
|
135
|
+
end
|
136
|
+
|
137
|
+
def method_missing(method_name, *args, &block)
|
138
|
+
if method_name.to_s =~ /^add_(.*)_scope$/
|
139
|
+
return send :add, $1.to_sym, *args, &block
|
140
|
+
end
|
141
|
+
super
|
142
|
+
end
|
143
|
+
|
144
|
+
def respond_to?(method_name)
|
145
|
+
return true if method_name.to_s =~ /^add_(.*)_scope$/
|
146
|
+
super
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
attr_reader :data_source, :scope_serialization
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
class Reporter::DataSource
|
3
|
+
|
4
|
+
def initialize *args, &block
|
5
|
+
@sources = []
|
6
|
+
@scopes = Reporter::DataSource::Scoping.new self
|
7
|
+
yield self if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
def << source
|
11
|
+
@sources << wrap_source(source)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
alias :add :<<
|
15
|
+
|
16
|
+
def get name
|
17
|
+
sources.detect { |source| source.name == name } or raise "Source #{name} not found"
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :scopes, :sources
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def wrap_source source
|
25
|
+
if source.ancestors.include? ActiveRecord::Base
|
26
|
+
Reporter::DataSource::ActiveRecordSource.new(self, source)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Reporter::DataStructure
|
2
|
+
|
3
|
+
def initialize data_set, *args
|
4
|
+
@data_set = data_set
|
5
|
+
@fields = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :fields
|
9
|
+
|
10
|
+
def << column_type, column_alias, *args, &block
|
11
|
+
klass = "reporter/field/#{column_type}".classify.constantize
|
12
|
+
column = klass.new self, column_alias, *args, &block
|
13
|
+
#TODO: Validate column
|
14
|
+
|
15
|
+
@fields[column_alias] = column
|
16
|
+
column
|
17
|
+
end
|
18
|
+
alias :add :<<
|
19
|
+
|
20
|
+
def field_value_of field, options
|
21
|
+
raise "No such field defined: #{field}" unless @fields.has_key? field
|
22
|
+
@fields[field].calculate_value(data_set.data_source, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method_name, *args, &block)
|
26
|
+
if method_name.to_s =~ /^add_(.*)_field$/
|
27
|
+
return send :add, "#{$1}_field", *args, &block
|
28
|
+
end
|
29
|
+
return send :add, :field, *args, &block if method_name.to_s == "add_field"
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to?(method_name)
|
34
|
+
return true if method_name.to_s.starts_with? "add_" and method_name.to_s.ends_with? "_field"
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :data_set
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Reporter::Field::Base
|
2
|
+
|
3
|
+
def initialize structure, alias_name
|
4
|
+
@structure = structure
|
5
|
+
@name = alias_name
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
def calculate_value data_source, options
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :name
|
17
|
+
|
18
|
+
protected
|
19
|
+
attr_reader :structure
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Reporter::Field::CalculationField < Reporter::Field::Base
|
2
|
+
|
3
|
+
def initialize structure, alias_name, data_source, calculation, column, options = {}, &block
|
4
|
+
super structure, alias_name
|
5
|
+
@source = data_source
|
6
|
+
@column = column
|
7
|
+
@options = options
|
8
|
+
@calculation = calculation
|
9
|
+
@calculation_block = block if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def calculate_value data_source, calculation_options
|
13
|
+
source = data_source.get(@source)
|
14
|
+
value = source.calculate @calculation, @column, options, &calculation_block
|
15
|
+
Reporter::Value.new(name, options[:name], value, nil, options[:description], options[:source_link])
|
16
|
+
end
|
17
|
+
|
18
|
+
def preload_for_period data_source, calculation_options, period, filter, scope
|
19
|
+
source = data_source.get(@source)
|
20
|
+
values = source.calculate_for_period @calculation, period, filter, scope, @column, options, &calculation_block
|
21
|
+
results = {}
|
22
|
+
values.each do |r|
|
23
|
+
val = r.delete :value
|
24
|
+
results[r] = Reporter::Value.new(name, options[:name], val, nil, options[:description], options[:source_link])
|
25
|
+
end
|
26
|
+
results
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :options, :calculation_block
|
32
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Reporter::Field::CountField < Reporter::Field::CalculationField
|
2
|
+
|
3
|
+
def initialize structure, alias_name, data_source, *args, &block
|
4
|
+
options = args.extract_options!
|
5
|
+
column = args.first
|
6
|
+
super structure, alias_name, data_source, :count, column, options, &block
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Reporter::Field::Field < Reporter::Field::Base
|
2
|
+
|
3
|
+
def initialize structure, alias_name, *args, &block
|
4
|
+
super structure, alias_name
|
5
|
+
@options = args.extract_options!
|
6
|
+
@value = args.first
|
7
|
+
@calculation_block = block if block_given?
|
8
|
+
end
|
9
|
+
|
10
|
+
def calculate_value data_source, calculation_options
|
11
|
+
if calculation_block
|
12
|
+
row = Reporter::Value.new(name, options[:name], nil, nil, options[:description], options[:source_link])
|
13
|
+
calculation_block.call(data_source, options, row)
|
14
|
+
return row
|
15
|
+
end
|
16
|
+
return Reporter::Value.new(name, options[:name], value, nil, options[:description], options[:source_link]) unless value.is_a? Symbol
|
17
|
+
Reporter::Value.new(name, options[:name], data_source.scopes.get(value).value,
|
18
|
+
data_source.scopes.get(value).human_name, options[:description], options[:source_link])
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :options, :value, :calculation_block
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Reporter::Field::FormulaField < Reporter::Field::Base
|
2
|
+
|
3
|
+
def initialize structure, alias_name, formula, options = {}
|
4
|
+
super structure, alias_name
|
5
|
+
@formula = Reporter::Formula.new formula
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def calculate_value data_source, calculation_options
|
10
|
+
required_terms = {}
|
11
|
+
formula.term_list.each do |term|
|
12
|
+
required_terms[term] = nil
|
13
|
+
required_terms[term] = calculation_options[:row][term].value if calculation_options[:row] and term != name
|
14
|
+
end
|
15
|
+
value = formula.call(required_terms)
|
16
|
+
|
17
|
+
Reporter::Value.new(name, options[:name], value, nil, options[:description], options[:source_link])
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :options, :formula
|
23
|
+
|
24
|
+
end
|