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,62 @@
|
|
1
|
+
module Reporter::Support::TimeRange
|
2
|
+
|
3
|
+
def time_range?
|
4
|
+
valid_types = [Date, DateTime, Time, ActiveSupport::TimeWithZone]
|
5
|
+
return false unless valid_types.include?(self.begin.class)
|
6
|
+
return false unless valid_types.include?(self.end.class)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def human_name
|
11
|
+
return inspect unless time_range?
|
12
|
+
b = test_begin_ends(self.begin)
|
13
|
+
e = test_begin_ends(self.end)
|
14
|
+
|
15
|
+
if self.begin.year == self.end.year
|
16
|
+
if b[:by] and e[:ey]
|
17
|
+
return self.begin.strftime("%Y")
|
18
|
+
elsif b[:bq] and e[:eq]
|
19
|
+
bq = get_quarter(self.begin)
|
20
|
+
eq = get_quarter(self.end)
|
21
|
+
if bq == eq
|
22
|
+
return I18n.t("time_range.quarter", :year => self.begin.year, :quarter => bq, :default => "q%{quarter} %{year}")
|
23
|
+
else
|
24
|
+
return I18n.t("time_range.multi_quarter", :year => self.begin.year,
|
25
|
+
:begin_quarter => bq, :end_quarter => eq, :default => "q%{begin_quarter} .. q%{end_quarter} %{year}")
|
26
|
+
end
|
27
|
+
elsif b[:bm] and e[:em]
|
28
|
+
if self.begin.month == self.end.month
|
29
|
+
return self.begin.strftime("%b '%y")
|
30
|
+
else
|
31
|
+
return "#{self.begin.strftime("%b")} .. #{self.end.strftime("%b '%y")}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else # multi year
|
35
|
+
if b[:by] and e[:ey]
|
36
|
+
return "#{self.begin.strftime("'%y")} .. #{self.end.strftime("'%y")}"
|
37
|
+
else
|
38
|
+
return inspect
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def get_quarter(date)
|
46
|
+
qs = [3, 6, 9, 12]
|
47
|
+
q = qs.detect { |q| date.month <= q }
|
48
|
+
(qs.index q) + 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_begin_ends(date)
|
52
|
+
r = {}
|
53
|
+
["year", "quarter", "month", "week"].each do |element|
|
54
|
+
r["b#{element.first}".to_sym] = (date.send("at_beginning_of_#{element}".to_sym) == date)
|
55
|
+
r["e#{element.first}".to_sym] = (date.send("at_end_of_#{element}".to_sym) == date)
|
56
|
+
end
|
57
|
+
r
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
Range.send(:include, Reporter::Support::TimeRange)
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Reporter::TimeIterator
|
2
|
+
|
3
|
+
public
|
4
|
+
|
5
|
+
VALID_TIME_STEP_SIZES = :total, :year, :quarter, :month, :week, :day
|
6
|
+
|
7
|
+
def iterate_time axis, *steps, &block
|
8
|
+
options = steps.extract_options!
|
9
|
+
scope = data_source.scopes.get axis
|
10
|
+
raise "Scope is not of Date type" unless scope.is_a? Reporter::Scope::DateScope
|
11
|
+
steps.each do |step|
|
12
|
+
raise "invalid stepsize: #{step}. must be one of #{VALID_TIME_STEP_SIZES.inspect}" unless VALID_TIME_STEP_SIZES.include? step
|
13
|
+
end
|
14
|
+
date_tree = build_date_tree steps
|
15
|
+
iterate_date_tree scope, date_tree, scope.limit, &block
|
16
|
+
scope.change nil
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
DATE_PART_VALUES = {
|
22
|
+
:total => 6,
|
23
|
+
:year => 5,
|
24
|
+
:quarter => 4,
|
25
|
+
:month => 3,
|
26
|
+
:week => 2,
|
27
|
+
:day => 1
|
28
|
+
}
|
29
|
+
|
30
|
+
def build_date_tree date_parts
|
31
|
+
date_parts = date_parts.dup
|
32
|
+
return nil if date_parts.empty?
|
33
|
+
# [6, 4, 3, 5]
|
34
|
+
# 2010 - 2011, q1, 1,2,3, q2, 4,5,6, q3, 7,8,9, q4, 10,11,12, 2010, q1, 1,2,3, q2, 4,5,6, q3, 7,8,9, q4, 10,11,12, 2011
|
35
|
+
date_coded = date_parts.collect { |part| DATE_PART_VALUES[part] }
|
36
|
+
if date_coded.first == date_coded.max
|
37
|
+
parent = date_parts.shift
|
38
|
+
{:children_first => false, :name => parent, :children => build_date_tree(date_parts)}
|
39
|
+
elsif date_coded.last == date_coded.max
|
40
|
+
parent = date_parts.pop
|
41
|
+
{:children_first => true, :name => parent, :children => build_date_tree(date_parts)}
|
42
|
+
else
|
43
|
+
raise "invalid sequence: #{date_parts.inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def iterate_date_tree scope, tree, time_frame, &block
|
48
|
+
iterate_time_periods time_frame, tree[:name] do |new_time_frame, optimization|
|
49
|
+
iterate_date_tree scope, tree[:children], time_frame, &block if tree[:children_first] and tree[:children]
|
50
|
+
scope.change new_time_frame
|
51
|
+
@time_iteration_row ||= Reporter::TimeOptimizedResultRow.new(self, nil, scope, time_frame)
|
52
|
+
@time_iteration_row.current_iteration = optimization
|
53
|
+
@time_iteration_row.scope = data_source.scopes.current_scope
|
54
|
+
yield @time_iteration_row
|
55
|
+
iterate_date_tree scope, tree[:children], time_frame, &block if !tree[:children_first] and tree[:children]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def iterate_time_periods period, block_type, &block
|
60
|
+
if block_type == :total
|
61
|
+
yield period
|
62
|
+
return
|
63
|
+
end
|
64
|
+
advancement, filter = case block_type
|
65
|
+
when :year : [{ :years => 1 }, [:year]]
|
66
|
+
when :quarter : [{ :months => 3 }, [:quarter, :year]]
|
67
|
+
when :month : [{ :months => 1 }, [:month, :year]]
|
68
|
+
when :week : [{ :weeks => 1 }, [:week, :year]]
|
69
|
+
when :day : [{ :days => 1 }, [:day, :month, :year]]
|
70
|
+
else raise "Unsupported type: #{block_type}"
|
71
|
+
end
|
72
|
+
iterate_period = period.begin.send("beginning_of_#{block_type}".to_sym) .. period.begin.send("end_of_#{block_type}".to_sym)
|
73
|
+
optimization = { :type => block_type, :filter => filter, :period => {} }
|
74
|
+
|
75
|
+
while iterate_period.begin < period.end
|
76
|
+
optimization[:period] = {}
|
77
|
+
optimization[:filter].each { |field| optimization[:period][field] = iterate_period.begin.send(field) }
|
78
|
+
|
79
|
+
yield iterate_period, optimization
|
80
|
+
iterate_period = iterate_period.begin.advance(advancement).send("beginning_of_#{block_type}".to_sym) ..
|
81
|
+
iterate_period.end.advance(advancement).send("end_of_#{block_type}".to_sym)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Reporter::TimeOptimizedResultRow < Reporter::ResultRow
|
2
|
+
|
3
|
+
def initialize(data_set, scope_serialization, scope, period)
|
4
|
+
super data_set, scope_serialization
|
5
|
+
@scope = scope
|
6
|
+
@period = period
|
7
|
+
@active_scope = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :current_iteration
|
11
|
+
|
12
|
+
def scope= scope
|
13
|
+
@scope_serialization = scope
|
14
|
+
end
|
15
|
+
|
16
|
+
def [] field
|
17
|
+
field_cache[field] ||= {}
|
18
|
+
preload_time_period_values_for field unless field_cache[field].has_key? current_iteration[:type]
|
19
|
+
field_cache[field][current_iteration[:type]] ||= {}
|
20
|
+
field_cache[field][current_iteration[:type]][current_iteration[:period]] ||= load_field_values(field)[field]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :data_set, :scope_serialization, :period
|
26
|
+
|
27
|
+
def preload_time_period_values_for(field_name)
|
28
|
+
#Rails.logger.info "Trying to pre-load data for #{field_name} for the period of #{period} in chunks of #{current_iteration[:filter].to_sentence}"
|
29
|
+
|
30
|
+
field = data_set.data_structure.fields[field_name]
|
31
|
+
if field.respond_to? :preload_for_period
|
32
|
+
#Rails.logger.info "Preloading possible for #{field_name}!"
|
33
|
+
field_cache[field_name] ||= {}
|
34
|
+
field_cache[field_name][current_iteration[:type]] = \
|
35
|
+
field.preload_for_period data_set.data_source, {}, period, current_iteration[:filter], @scope
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Reporter::Value
|
2
|
+
|
3
|
+
def initialize(field_alias, field_human_name, value, human_value, description, source_link)
|
4
|
+
@field_alias = field_alias
|
5
|
+
@field_human_name = field_human_name || field_alias
|
6
|
+
@value = value
|
7
|
+
@human_value = human_value
|
8
|
+
@description = description
|
9
|
+
@source_link = source_link
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :field_alias, :field_human_name
|
13
|
+
attr_accessor :value, :description, :source_link
|
14
|
+
attr_writer :human_value
|
15
|
+
|
16
|
+
def human_value
|
17
|
+
@human_value || value
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
human_value
|
22
|
+
end
|
23
|
+
|
24
|
+
def as_percentage
|
25
|
+
if @value.is_a? Numeric
|
26
|
+
"%.2f %%" % (@value * 100.0)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def round(precision = 2)
|
31
|
+
if @value.is_a? Numeric
|
32
|
+
"%.#{precision}f" % @value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reporter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matthijs Groen
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-09-16 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
- 0
|
34
|
+
version: 3.0.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: activesupport
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 7
|
46
|
+
segments:
|
47
|
+
- 3
|
48
|
+
- 0
|
49
|
+
- 0
|
50
|
+
version: 3.0.0
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: arel
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 21
|
62
|
+
segments:
|
63
|
+
- 1
|
64
|
+
- 0
|
65
|
+
- 1
|
66
|
+
version: 1.0.1
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
description: "\n Reporter adds a consistent way to build reports.\n "
|
70
|
+
email: matthijs.groen@gmail.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files:
|
76
|
+
- README.markdown
|
77
|
+
files:
|
78
|
+
- Gemfile
|
79
|
+
- MIT-LICENSE
|
80
|
+
- README.markdown
|
81
|
+
- Rakefile
|
82
|
+
- VERSION
|
83
|
+
- lib/reporter/data_set.rb
|
84
|
+
- lib/reporter/data_source.rb
|
85
|
+
- lib/reporter/data_source/active_record_source.rb
|
86
|
+
- lib/reporter/data_source/scoping.rb
|
87
|
+
- lib/reporter/data_structure.rb
|
88
|
+
- lib/reporter/field/average_field.rb
|
89
|
+
- lib/reporter/field/base.rb
|
90
|
+
- lib/reporter/field/calculation_field.rb
|
91
|
+
- lib/reporter/field/count_field.rb
|
92
|
+
- lib/reporter/field/field.rb
|
93
|
+
- lib/reporter/field/formula_field.rb
|
94
|
+
- lib/reporter/field/sum_field.rb
|
95
|
+
- lib/reporter/formula.rb
|
96
|
+
- lib/reporter/result_row.rb
|
97
|
+
- lib/reporter/scope/base.rb
|
98
|
+
- lib/reporter/scope/date_scope.rb
|
99
|
+
- lib/reporter/scope/reference_scope.rb
|
100
|
+
- lib/reporter/support/time_range.rb
|
101
|
+
- lib/reporter/time_iterator.rb
|
102
|
+
- lib/reporter/time_optimized_result_row.rb
|
103
|
+
- lib/reporter/value.rb
|
104
|
+
has_rdoc: true
|
105
|
+
homepage: http://github.com/matthijsgroen/reporter
|
106
|
+
licenses: []
|
107
|
+
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options:
|
110
|
+
- --charset=UTF-8
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
hash: 3
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
version: "0"
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
hash: 3
|
128
|
+
segments:
|
129
|
+
- 0
|
130
|
+
version: "0"
|
131
|
+
requirements: []
|
132
|
+
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.3.7
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: Report builder.
|
138
|
+
test_files: []
|
139
|
+
|