reporter 0.0.1
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.
- 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
|