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,371 @@
|
|
1
|
+
# Formula parser and calculator
|
2
|
+
# Author:: Matthijs Groen
|
3
|
+
#
|
4
|
+
# This class has two main functions:
|
5
|
+
# 1. to parse formula into ready-to-use-arrays
|
6
|
+
# 2. use those arrays to perform calculations
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# = Parsing formula
|
10
|
+
# my_formula = Formula.new("100% – (MAX(score – 5, 0) * 10%)") => Formula
|
11
|
+
# my_formula_data = Formula.make("100% – (MAX(score – 5, 0) * 10%)") => Array
|
12
|
+
#
|
13
|
+
# The array format used for formula data is [:operator, [parameter, parameter]]
|
14
|
+
# the parameters can also be arrays: e.g. sub-calculations
|
15
|
+
#
|
16
|
+
# The text formula can be build with the following elements:
|
17
|
+
# == operators:
|
18
|
+
# -:: subtract. subtracts the right side from the left side argument.
|
19
|
+
# *:: multiply. multiplies the left side with the right side argument.
|
20
|
+
# /:: divide. divides the left side with the ride side argument.
|
21
|
+
# +:: add. adds the right side to the left side argument.
|
22
|
+
# ^:: power. multiplies the left side by the power of the right side.
|
23
|
+
#
|
24
|
+
# == functions:
|
25
|
+
# functions have the format of name(parameters)
|
26
|
+
# the parameters of the function will be pre calculated before the code of the function is executed.
|
27
|
+
# supported functions:
|
28
|
+
#
|
29
|
+
# max:: selects the biggest value from the provided values
|
30
|
+
# min:: selects the smallest value from the provided values
|
31
|
+
# sum:: creates a sum of all the provided values
|
32
|
+
# avg:: creates an average of all the provided values
|
33
|
+
# select:: selects the value with the index of the first parameter
|
34
|
+
# empty:: returns 1 if the given string is empty, 0 otherwise
|
35
|
+
#
|
36
|
+
# == parenthesis:
|
37
|
+
# parentesis can be used to group calculation parts
|
38
|
+
#
|
39
|
+
# == variables:
|
40
|
+
# terms that start with a alfabetic character and contain only alfanumeric characters and underscores
|
41
|
+
# can be used as variables. A hash with variables should be supplied when the calculation is performed
|
42
|
+
#
|
43
|
+
# == numeric values:
|
44
|
+
# numeric values like integers, floats and percentages are also allowed. Percentages will be converted to floats.
|
45
|
+
# 3% and 66% will be converted to resp. 100% / 3 and 200% / 3
|
46
|
+
#
|
47
|
+
# = Performing calculations
|
48
|
+
# my_formula.call(:score => 7.0) => 0.8 (using the above formula example)
|
49
|
+
# Formula.calculate(my_formula_data, :score => 3.0) => 1.0 (using the above formula example)
|
50
|
+
#
|
51
|
+
class Reporter::Formula
|
52
|
+
|
53
|
+
# Known operators
|
54
|
+
OPERATORS = "-*/+^"
|
55
|
+
|
56
|
+
# parse the given code formula in an array using the format
|
57
|
+
# calculation = [operation, [parameter, parameter]]
|
58
|
+
# a parameter can ofcourse be in turn another calculation
|
59
|
+
def initialize(code)
|
60
|
+
@calculation = Reporter::Formula.make code
|
61
|
+
#puts "#{@calculation.inspect}"
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :calculation
|
65
|
+
|
66
|
+
# Parses the given formula as text and returns the formula in nested array form.
|
67
|
+
def self.make(code)
|
68
|
+
#puts "parsing: #{code}"
|
69
|
+
begin
|
70
|
+
parse_operation(code)
|
71
|
+
rescue StandardError => e
|
72
|
+
puts "Error in formula: #{code}: #{e}"
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# executes the formula with a hash of given calculation terms
|
78
|
+
def call(input)
|
79
|
+
begin
|
80
|
+
Reporter::Formula.calculate(@calculation, input)
|
81
|
+
rescue StandardError => e
|
82
|
+
Rails.logger.error "Error executing formula: #{Reporter::Formula.calculation_to_s(@calculation, input)} : #{e.message}"
|
83
|
+
raise
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def term_list
|
88
|
+
terms = {}
|
89
|
+
Reporter::Formula.term_list @calculation, terms
|
90
|
+
terms.keys
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_string(input)
|
94
|
+
Reporter::Formula.calculation_to_s(@calculation, input)
|
95
|
+
end
|
96
|
+
|
97
|
+
def solve(input)
|
98
|
+
Reporter::Formula.calculation_to_s(@calculation, input, true)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.calculation_to_s(calculation, input, solve = false)
|
102
|
+
operation, parameters = * calculation
|
103
|
+
|
104
|
+
string_parameters = parameters.collect do |parameter|
|
105
|
+
parameter.is_a?(Array) ? "#{calculation_to_s(parameter, input, solve)}" : parameter
|
106
|
+
end
|
107
|
+
case operation
|
108
|
+
when :add,
|
109
|
+
:subtract,
|
110
|
+
:times,
|
111
|
+
:divide,
|
112
|
+
:power then
|
113
|
+
"(#{string_parameters[0]} #{{:add => "+",
|
114
|
+
:subtract => "-",
|
115
|
+
:times => "*",
|
116
|
+
:divide => "/",
|
117
|
+
:power => "^"}[operation]} #{string_parameters[1]})"
|
118
|
+
# functions:
|
119
|
+
when :max,
|
120
|
+
:min,
|
121
|
+
:sum,
|
122
|
+
:select,
|
123
|
+
:avg,
|
124
|
+
:empty then
|
125
|
+
if solve
|
126
|
+
result = calculate(calculation, input)
|
127
|
+
"#{operation}(#{string_parameters * ","})[#{result}]"
|
128
|
+
else
|
129
|
+
"#{operation}(#{string_parameters * ","})"
|
130
|
+
end
|
131
|
+
# variables
|
132
|
+
when :text then
|
133
|
+
"\"#{string_parameters[0]}\""
|
134
|
+
when :term then
|
135
|
+
"#{string_parameters[0]}[#{input[string_parameters[0]] ? input[string_parameters[0]] : "nil"}]"
|
136
|
+
when :negative_term then
|
137
|
+
"-#{string_parameters[0]}[#{input[string_parameters[0]] ? input[string_parameters[0]] : "nil"}]"
|
138
|
+
when :literal then
|
139
|
+
begin
|
140
|
+
"nil" if string_parameters[0].nil?
|
141
|
+
end
|
142
|
+
# no-op
|
143
|
+
when nil then
|
144
|
+
string_parameters[0].to_s
|
145
|
+
when :percentage then
|
146
|
+
"#{string_parameters[0] * 100.0}%"
|
147
|
+
else
|
148
|
+
"!unsupported(#{operation}}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.term_list(calculation, input = {})
|
153
|
+
operation, parameters = *calculation
|
154
|
+
|
155
|
+
parameters = parameters.collect do |parameter|
|
156
|
+
parameter.is_a?(Array) ? term_list(parameter, input) : parameter
|
157
|
+
end
|
158
|
+
|
159
|
+
case operation
|
160
|
+
# variables
|
161
|
+
when :term then
|
162
|
+
input[parameters[0]] = :term
|
163
|
+
when :negative_term then
|
164
|
+
input[parameters[0]] = :term
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.calculate(calculation, input)
|
169
|
+
operation, parameters = *calculation
|
170
|
+
|
171
|
+
parameters = parameters.collect do |parameter|
|
172
|
+
parameter.is_a?(Array) ? calculate(parameter, input) : parameter
|
173
|
+
end
|
174
|
+
|
175
|
+
return nil if (parameters[0].nil? or parameters[1].nil?) and [:add, :subtract, :times, :divide, :power].include? operation
|
176
|
+
|
177
|
+
case operation
|
178
|
+
when :add then
|
179
|
+
parameters[0] + parameters[1]
|
180
|
+
when :subtract then
|
181
|
+
parameters[0] - parameters[1]
|
182
|
+
when :times then
|
183
|
+
parameters[0] * parameters[1]
|
184
|
+
when :divide then
|
185
|
+
parameters[1] == 0 ? nil : parameters[0].to_f / parameters[1].to_f
|
186
|
+
when :power then
|
187
|
+
parameters[0] ** parameters[1]
|
188
|
+
# functions:
|
189
|
+
when :max then
|
190
|
+
parameters.compact.max
|
191
|
+
when :min then
|
192
|
+
parameters.compact.min
|
193
|
+
when :sum then
|
194
|
+
begin
|
195
|
+
result = 0.0
|
196
|
+
parameters.each { |value| result += value || 0.0 }
|
197
|
+
result
|
198
|
+
end
|
199
|
+
when :select then
|
200
|
+
begin
|
201
|
+
index = parameters.shift
|
202
|
+
index.is_a?(Numeric) ? parameters[index - 1] : nil
|
203
|
+
end
|
204
|
+
when :avg then
|
205
|
+
begin
|
206
|
+
items = parameters.compact
|
207
|
+
result = 0.0
|
208
|
+
items.each { |value| result += value }
|
209
|
+
result / items.length
|
210
|
+
end
|
211
|
+
when :empty then
|
212
|
+
begin
|
213
|
+
result = parameters.collect { |item| item.to_s.strip == "" ? 1 : 0 }
|
214
|
+
result.include?(0) ? 0 : 1
|
215
|
+
end
|
216
|
+
# variables
|
217
|
+
when :term then
|
218
|
+
begin
|
219
|
+
raise "Can't find term: #{parameters[0]}. Has keys: #{input.keys.collect(&:to_s).sort.inspect}" unless input.has_key? parameters[0]
|
220
|
+
input[parameters[0]]
|
221
|
+
end
|
222
|
+
when :negative_term then
|
223
|
+
begin
|
224
|
+
raise "Can't find term: #{parameters[0]}. Has keys: #{input.keys.sort.inspect}" unless input.has_key? parameters[0]
|
225
|
+
val = input[parameters[0]]
|
226
|
+
return nil unless val
|
227
|
+
- val
|
228
|
+
end
|
229
|
+
when :literal
|
230
|
+
parameters[0]
|
231
|
+
when :text
|
232
|
+
parameters[0]
|
233
|
+
# no-op
|
234
|
+
when nil, :percentage then
|
235
|
+
parameters[0].to_f
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def self.parse_operation(code)
|
242
|
+
#puts "parsing: #{code}"
|
243
|
+
|
244
|
+
# check if the code is totally surrounded by parenthesis that can be removed. remove them if possible
|
245
|
+
code = ungroup code
|
246
|
+
|
247
|
+
left, right, operator, operator_char = "", "", nil, ""
|
248
|
+
char_index, group_level, in_text = 0, 0, false
|
249
|
+
while char_index < code.length
|
250
|
+
char = code[char_index, 1]
|
251
|
+
if operator.nil? and OPERATORS.include? char and group_level == 0 and not in_text
|
252
|
+
operator = case char
|
253
|
+
when "-" then :subtract
|
254
|
+
when "+" then :add
|
255
|
+
when "*" then :times
|
256
|
+
when "/" then :divide
|
257
|
+
when "^" then :power
|
258
|
+
end
|
259
|
+
operator_char = char
|
260
|
+
else
|
261
|
+
in_text = !in_text if char == "\""
|
262
|
+
group_level += (char == "(") ? 1 : -1 if "()".include? char and not in_text
|
263
|
+
operator ? right += char : left += char
|
264
|
+
end
|
265
|
+
char_index += 1
|
266
|
+
end
|
267
|
+
begin
|
268
|
+
#puts "parse-result: #{operator}, #{left}, #{right}"
|
269
|
+
|
270
|
+
return parse_definition(left.strip) unless operator
|
271
|
+
return parse_definition(operator_char + right) if operator and left.strip == ""
|
272
|
+
return operator, [parse_operation(left.strip), parse_operation(right.strip)]
|
273
|
+
rescue StandardError => e
|
274
|
+
puts "can't parse code: \"#{code}\""
|
275
|
+
raise
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def self.parse_definition(code)
|
280
|
+
code = code.strip
|
281
|
+
|
282
|
+
# text "some text"
|
283
|
+
if result = code.match(/\A"([^"]*)"\z/)
|
284
|
+
return :text, result[1]
|
285
|
+
|
286
|
+
# parse percentages 100%, 10%
|
287
|
+
elsif result = code.match(/\A([\d\.]+)%\z/)
|
288
|
+
return :percentage, [1.0 / 3.0] if result[1].to_f == 33.0
|
289
|
+
return :percentage, [2.0 / 3.0] if result[1].to_f == 66.0
|
290
|
+
return :percentage, [result[1].to_f / 100.0]
|
291
|
+
|
292
|
+
# parse function calls in the format FUNCTION(parameters)
|
293
|
+
elsif result = code.upcase.match(/\A([A-Z_]+)\((.+)\)\z/m)
|
294
|
+
return result[1].downcase.to_sym, self.parameterize(result[2][0 .. -1]).collect { |parameter| parse_operation(parameter) }
|
295
|
+
|
296
|
+
# parse numeric value
|
297
|
+
elsif code.to_i.to_s == code
|
298
|
+
return nil, [code.to_i]
|
299
|
+
|
300
|
+
# parse numeric value
|
301
|
+
elsif code.to_f.to_s == code
|
302
|
+
return nil, [code.to_f]
|
303
|
+
|
304
|
+
# parse literal
|
305
|
+
elsif result = code.upcase.match(/\ANIL\z/)
|
306
|
+
return :literal, [nil]
|
307
|
+
|
308
|
+
# parse variable term
|
309
|
+
elsif result = code.upcase.match(/\A([A-Z][A-Z0-9_]*)\z/)
|
310
|
+
return :term, [result[1].downcase.to_sym]
|
311
|
+
elsif result = code.upcase.match(/\A-([A-Z][A-Z0-9_]*)\z/)
|
312
|
+
return :negative_term, [result[1].downcase.to_sym]
|
313
|
+
else
|
314
|
+
raise "can't parse code: \"#{code}\""
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# check if the code is totally surrounded by parenthesis that can be removed. remove them if possible
|
319
|
+
# examples:
|
320
|
+
# ungroup("(my code ()") => "(my code ()"
|
321
|
+
# ungroup("(my code ())") => "my code ()"
|
322
|
+
# ungroup("(my code) ()") => "(my code) ()"
|
323
|
+
# ungroup("m(my code)") => "m(my code)"
|
324
|
+
def self.ungroup(code)
|
325
|
+
# exit if the code does not start with an opening parentesis
|
326
|
+
return code unless code[0, 1] == "("
|
327
|
+
return code unless code[-1, 1] == ")"
|
328
|
+
# since we know the first character is an opening parenthesis,
|
329
|
+
# start parsing at the second character, and assume grouping level 1
|
330
|
+
group_level, char_index, in_text = 1, 1, false
|
331
|
+
while char_index < code.length
|
332
|
+
char = code[char_index, 1]
|
333
|
+
in_text = !in_text if char == "\""
|
334
|
+
group_level += 1 if char == "(" and not in_text
|
335
|
+
group_level -= 1 if char == ")" and not in_text
|
336
|
+
|
337
|
+
# only strip the first and last parenthesis if we exit the grouping AND we reached the last character
|
338
|
+
return code[1 .. -2] if group_level == 0 and char_index == code.length - 1
|
339
|
+
char_index += 1
|
340
|
+
end
|
341
|
+
code
|
342
|
+
end
|
343
|
+
|
344
|
+
#
|
345
|
+
# SUM(1, 2), SELECT(1, 2, 3)
|
346
|
+
def self.parameterize(code)
|
347
|
+
result = []
|
348
|
+
# since we know the first character is an opening parenthesis,
|
349
|
+
# start parsing at the second character, and assume grouping level 1
|
350
|
+
current_param, char = "", ""
|
351
|
+
group_level, char_index, in_text = 0, 0, false
|
352
|
+
while char_index <= code.length
|
353
|
+
char = code[char_index, 1]
|
354
|
+
in_text = !in_text if char == "\""
|
355
|
+
group_level += 1 if char == "(" and not in_text
|
356
|
+
group_level -= 1 if char == ")" and not in_text
|
357
|
+
|
358
|
+
if char == "," and group_level == 0 and not in_text
|
359
|
+
result << current_param
|
360
|
+
current_param = ""
|
361
|
+
else
|
362
|
+
current_param << char
|
363
|
+
end
|
364
|
+
|
365
|
+
char_index += 1
|
366
|
+
end
|
367
|
+
result << current_param unless current_param == ""
|
368
|
+
result
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Reporter::ResultRow
|
2
|
+
|
3
|
+
def initialize(data_set, scope_serialization)
|
4
|
+
@data_set = data_set
|
5
|
+
@scope_serialization = scope_serialization
|
6
|
+
@field_cache = { }
|
7
|
+
end
|
8
|
+
|
9
|
+
def [] field
|
10
|
+
field_cache[field] ||= load_field_values(field)[field]
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
attr_reader :data_set, :scope_serialization
|
16
|
+
attr_accessor :field_cache
|
17
|
+
|
18
|
+
def load_field_values(*fields)
|
19
|
+
execute_fields *(fields + [{ :scope => scope_serialization, :row => self }])
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute_fields *fields
|
23
|
+
options = fields.extract_options!
|
24
|
+
temp_scope = data_set.data_source.scopes.current_scope
|
25
|
+
field_options = {}
|
26
|
+
data_set.data_source.scopes.apply_scope options[:scope] if options[:scope]
|
27
|
+
field_options[:row] = options[:row] if options[:row]
|
28
|
+
results = {}
|
29
|
+
fields.each do |field|
|
30
|
+
results[field] = data_set.data_structure.field_value_of field, field_options
|
31
|
+
end
|
32
|
+
data_set.data_source.scopes.apply_scope temp_scope
|
33
|
+
results
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Reporter::Scope::Base
|
2
|
+
|
3
|
+
def initialize scoping, name, data_source, mappings
|
4
|
+
@data_source = data_source
|
5
|
+
@scoping = scoping
|
6
|
+
@name = name
|
7
|
+
@mappings = scoping.normalize_mapping mappings
|
8
|
+
@limit = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def limit= *args
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def change value
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply_on source
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def iterate &block
|
28
|
+
raise NotImplementedError
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :name, :mappings, :limit
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
attr_reader :scoping, :data_source
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
class Reporter::Scope::DateScope < Reporter::Scope::Base
|
2
|
+
|
3
|
+
def initialize scoping, name, data_source, mappings
|
4
|
+
super scoping, name, data_source, mappings
|
5
|
+
@period = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def limit= period
|
9
|
+
@limit = period_as_range(period)
|
10
|
+
end
|
11
|
+
|
12
|
+
def value
|
13
|
+
active_period
|
14
|
+
end
|
15
|
+
|
16
|
+
def human_name
|
17
|
+
active_period.human_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def active_period
|
21
|
+
get_period || @limit
|
22
|
+
end
|
23
|
+
|
24
|
+
def change period
|
25
|
+
scoping.serialize_scope name, period_as_range(period)
|
26
|
+
end
|
27
|
+
|
28
|
+
def apply_on source, period = nil
|
29
|
+
raise "No mapped column for source #{source}" unless mappings.has_key? source.name
|
30
|
+
column = mappings[source.name]
|
31
|
+
# The time period range is inclusive in all aspects of the report to detemine correctly if it runs to the end of a year
|
32
|
+
# or the beginning of another year.
|
33
|
+
# In SQL, the period is used as a BETWEEN statement, with the end exclusive.
|
34
|
+
# In code when we want the whole of Januari, we use 1-1 00:00 till 1-31 23:59
|
35
|
+
# in SQL, we need to use BETWEEN 1-1 00:00 AND 2-1 00:00
|
36
|
+
# when a date includes time, we need to add 1 second. If a date has no time, we need to include 1 day.
|
37
|
+
|
38
|
+
period ||= active_period.dup
|
39
|
+
period = if period.end.is_a? Date
|
40
|
+
period.begin .. period.end.advance(:days => 1)
|
41
|
+
else
|
42
|
+
period.begin .. period.end.advance(:seconds => 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
case column
|
46
|
+
when String :
|
47
|
+
source.where(column.to_sym => period)
|
48
|
+
when Hash : begin
|
49
|
+
column.each do |key, value|
|
50
|
+
source = source.joins(key).where(key => { value => period })
|
51
|
+
end
|
52
|
+
source
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def group_on source, period_type
|
58
|
+
column = mappings[source.name]
|
59
|
+
case column
|
60
|
+
when String :
|
61
|
+
"#{period_type.to_s.upcase}(#{column})"
|
62
|
+
when Hash : begin
|
63
|
+
column.each do |key, value|
|
64
|
+
table_name = table_name_of_association source, key
|
65
|
+
return "#{period_type.to_s.upcase}(#{table_name}.#{value})"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.possible_scopes sources
|
72
|
+
results = []
|
73
|
+
global_dates = nil
|
74
|
+
specific_dates = { :type => :date, :match => :loose }
|
75
|
+
sources.each do |source|
|
76
|
+
global_dates = global_dates ? (global_dates & source.date_columns) : source.date_columns
|
77
|
+
specific_dates[source.model_name] = source.date_columns
|
78
|
+
end
|
79
|
+
results << specific_dates
|
80
|
+
global_dates.each do |reference|
|
81
|
+
result_hash = {:type => :date, :match => :exact}
|
82
|
+
sources.each { |source| result_hash[source.model_name] = reference }
|
83
|
+
results << result_hash
|
84
|
+
end if global_dates
|
85
|
+
results
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def get_period
|
91
|
+
scoping.unserialize_scope name
|
92
|
+
end
|
93
|
+
|
94
|
+
def period_as_range period
|
95
|
+
case period
|
96
|
+
when Range :
|
97
|
+
period.dup
|
98
|
+
when Fixnum :
|
99
|
+
Date.civil(period).beginning_of_year .. Date.civil(period).end_of_year
|
100
|
+
when :year_cumulative :
|
101
|
+
active_period.begin.beginning_of_year .. active_period.end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def table_name_of_association source, name
|
106
|
+
source.reflect_on_association(name.to_sym).klass.table_name
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
class Reporter::Scope::ReferenceScope < Reporter::Scope::Base
|
2
|
+
|
3
|
+
def initialize scoping, name, data_source, *args
|
4
|
+
mappings = args.extract_options!
|
5
|
+
mappings = create_mappings_for_object_type(data_source, args.first, mappings) if args.first
|
6
|
+
super scoping, data_source, name, mappings
|
7
|
+
@limiter = nil
|
8
|
+
@id_collection = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def limit= object_or_array
|
12
|
+
@limiter = object_or_array
|
13
|
+
end
|
14
|
+
|
15
|
+
def group_object
|
16
|
+
scoping.unserialize_scope(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
group_object || @limiter
|
21
|
+
end
|
22
|
+
|
23
|
+
def change object
|
24
|
+
scoping.serialize_scope name, item
|
25
|
+
end
|
26
|
+
|
27
|
+
def human_name
|
28
|
+
limiter = group_object || @limiter
|
29
|
+
return limiter.collect { |item| human_name_for item }.to_sentence if limiter.is_a? Enumerable
|
30
|
+
human_name_for limiter
|
31
|
+
end
|
32
|
+
|
33
|
+
def apply_on source
|
34
|
+
raise "No mapped column for source #{source}" unless mappings.has_key? source.name
|
35
|
+
reference = mappings[source.name]
|
36
|
+
|
37
|
+
limiter = group_object || @limiter
|
38
|
+
if limiter
|
39
|
+
q, values = limit_through_association source, reference, limiter
|
40
|
+
source.where(q, values)
|
41
|
+
else
|
42
|
+
source
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def iterate items, data_set, &block
|
47
|
+
items ||= @limiter
|
48
|
+
if items.is_a? Enumerable
|
49
|
+
items.each do |item|
|
50
|
+
scoping.serialize_scope name, item
|
51
|
+
yield data_set.get_row
|
52
|
+
end
|
53
|
+
scoping.serialize_scope name, nil
|
54
|
+
else
|
55
|
+
scoping.serialize_scope name, items
|
56
|
+
yield data_set.get_row
|
57
|
+
scoping.serialize_scope name, nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.possible_scopes sources
|
62
|
+
results = []
|
63
|
+
# check reflections
|
64
|
+
reflection_references = nil
|
65
|
+
sources.each do |source|
|
66
|
+
relations = source.relations_to_objects
|
67
|
+
relation_pool = relations.keys + [source.active_record]
|
68
|
+
reflection_references = reflection_references ? (reflection_references & relation_pool) : relation_pool
|
69
|
+
end
|
70
|
+
(reflection_references || []).each do |reflection_object|
|
71
|
+
result_hash = { :type => :reference, :match => :loose, :object => reflection_object }
|
72
|
+
sources.each do |source|
|
73
|
+
fields = []
|
74
|
+
fields << "id" if (source.active_record == reflection_object)
|
75
|
+
fields += source.relations_to_objects[reflection_object] || []
|
76
|
+
result_hash[source.model_name] = fields
|
77
|
+
end
|
78
|
+
results << result_hash
|
79
|
+
end
|
80
|
+
#Rails.logger.info reflection_references.inspect
|
81
|
+
|
82
|
+
results
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def limit_through_association source, reference, limiter
|
88
|
+
if reference.is_a? Array
|
89
|
+
query = [[], {}]
|
90
|
+
reference.each do |ref|
|
91
|
+
q, values = limit_through_association source, ref, limiter
|
92
|
+
query[0] << q
|
93
|
+
query[1].merge! values
|
94
|
+
end
|
95
|
+
return "(#{query[0].join ") OR ("})", query[1]
|
96
|
+
end
|
97
|
+
association = source.reflect_on_association(reference.to_sym)
|
98
|
+
if association.macro == :belongs_to
|
99
|
+
id_collection = get_ids_from limiter, reference, association
|
100
|
+
#Rails.logger.info "Belongs to association #{reference} limited by #{limiter}: #{id_collection.inspect}"
|
101
|
+
return "#{reference}_id IN(:#{reference}_ids)", { "#{reference}_ids".to_sym => id_collection }
|
102
|
+
elsif association.macro == :has_one and association.options[:through]
|
103
|
+
#Rails.logger.info "Has one through association #{reference} limited by #{limiter}"
|
104
|
+
through_association = association.options[:through]
|
105
|
+
limit_through_association source, through_association, limiter
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def get_ids_from item, reference, association
|
110
|
+
return [] if item.nil?
|
111
|
+
return item.collect { |sub_item| get_ids_from sub_item, reference, association }.flatten if item.is_a? Enumerable
|
112
|
+
if item.class.ancestors.include? ActiveRecord::Base
|
113
|
+
|
114
|
+
return [item.id] if item.class == association.klass
|
115
|
+
return item.send("#{reference.to_s}_ids".to_sym) if item.respond_to? "#{reference.to_s}_ids"
|
116
|
+
return [item.send("#{reference.to_s}_id".to_sym)] if item.respond_to? "#{reference.to_s}_id"
|
117
|
+
end
|
118
|
+
[]
|
119
|
+
end
|
120
|
+
|
121
|
+
def human_name_for item
|
122
|
+
item.name
|
123
|
+
end
|
124
|
+
|
125
|
+
def create_mappings_for_object_type data_source, object, mappings
|
126
|
+
possible = self.class.possible_scopes data_source.sources
|
127
|
+
#Rails.logger.info possible.inspect
|
128
|
+
possible.each do |reference_mapping|
|
129
|
+
return create_mapping_from(reference_mapping, mappings, data_source.sources) if reference_mapping[:object] == object
|
130
|
+
end
|
131
|
+
raise "No valid data-source mapping could be made with #{object.name}"
|
132
|
+
end
|
133
|
+
|
134
|
+
def create_mapping_from reference_mapping, mapping_specifics, sources
|
135
|
+
mapping = {}
|
136
|
+
sources.each do |source|
|
137
|
+
key = source.model_name.underscore.to_sym
|
138
|
+
columns = reference_mapping[source.model_name]
|
139
|
+
if columns.size == 1
|
140
|
+
mapping[key] = columns.first
|
141
|
+
else
|
142
|
+
specifics = [mapping_specifics[key]].flatten
|
143
|
+
specifics.each do |specific|
|
144
|
+
raise "No available reference to satisfy one of these columns (#{columns.to_sentence}) for datasource #{source.model_name}" unless specific and columns.include? specific
|
145
|
+
end
|
146
|
+
mapping[key] = mapping_specifics[key]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
#{ :funeral => "work_area", :cbs_statistic => "work_area" }
|
150
|
+
#Rails.logger.info mapping.inspect
|
151
|
+
mapping
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|