kbaum-munger 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ module Munger #:nodoc:
2
+
3
+ class Item
4
+
5
+ attr_reader :data
6
+
7
+ def initialize(data)
8
+ @data = data
9
+ end
10
+
11
+ def [](key)
12
+ return @data[key] if @data[key]
13
+ if key.is_a? Symbol
14
+ return @data[key.to_s] if @data[key.to_s]
15
+ elsif key.is_a? String
16
+ return @data[key.to_sym] if @data[key.to_sym]
17
+ end
18
+ end
19
+
20
+ def []=(key, value)
21
+ @data[key] = value
22
+ end
23
+
24
+ def method_missing( id, *args )
25
+ if @data[id].nil?
26
+ m = id.to_s
27
+ if /=$/ =~ m
28
+ @data[m.chomp!] = (args.length < 2 ? args[0] : args)
29
+ else
30
+ @data[m]
31
+ end
32
+ else
33
+ @data[id]
34
+ end
35
+ end
36
+
37
+ def self.ensure(item)
38
+ if item.is_a? Munger::Item
39
+ return item
40
+ else
41
+ return Item.new(item)
42
+ end
43
+ end
44
+
45
+ def to_hash
46
+ @data
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ # Munger::Render.to_html(report)
2
+
3
+ module Munger #:nodoc:
4
+ module Render
5
+
6
+ def self.to_html(report, options = {})
7
+ Html::new(report, options).render
8
+ end
9
+
10
+ def self.to_sortable_html(report, options ={})
11
+ SortableHtml::new(report, options).render
12
+ end
13
+
14
+ def self.to_text(report)
15
+ Text::new(report).render
16
+ end
17
+
18
+ def self.to_csv(report)
19
+ CSV::new(report).render
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ module Munger #:nodoc:
2
+ module Render #:nodoc:
3
+ class CSV #:nodoc:
4
+
5
+ attr_reader :report
6
+
7
+ def initialize(report)
8
+ @report = report
9
+ end
10
+
11
+ def render
12
+ output = []
13
+
14
+ # header
15
+ output << @report.columns.collect { |col| @report.column_title(col).to_s }.join(',')
16
+
17
+ # body
18
+ @report.process_data.each do |row|
19
+ output << @report.columns.collect { |col| row[:data][col].to_s }.join(',')
20
+ end
21
+
22
+ output.join("\n")
23
+ end
24
+
25
+ def valid?
26
+ @report.is_a? Munger::Report
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,89 @@
1
+ begin
2
+ require 'builder'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'builder'
6
+ end
7
+
8
+ module Munger #:nodoc:
9
+ module Render #:nodoc:
10
+ class Html
11
+
12
+ attr_reader :report, :classes
13
+
14
+ def initialize(report, options = {})
15
+ @report = report
16
+ set_classes(options[:classes])
17
+ end
18
+
19
+ def set_classes(options = nil)
20
+ options = {} if !options
21
+ default = {:table => 'report-table'}
22
+ @classes = default.merge(options)
23
+ end
24
+
25
+ def render
26
+ x = Builder::XmlMarkup.new
27
+
28
+ x.table(:class => @classes[:table]) do
29
+
30
+ x.tr do
31
+ @report.columns.each do |column|
32
+ x.th(:class => 'columnTitle') { x << @report.column_title(column) }
33
+ end
34
+ end
35
+
36
+ @report.process_data.each do |row|
37
+
38
+ classes = []
39
+ classes << row[:meta][:row_styles]
40
+ classes << 'group' + row[:meta][:group].to_s if row[:meta][:group]
41
+ classes << cycle('even', 'odd')
42
+ classes.compact!
43
+
44
+ if row[:meta][:group_header]
45
+ classes << 'groupHeader' + row[:meta][:group_header].to_s
46
+ end
47
+
48
+ row_attrib = {}
49
+ row_attrib = {:class => classes.join(' ')} if classes.size > 0
50
+
51
+ x.tr(row_attrib) do
52
+ if row[:meta][:group_header]
53
+ header = row[:meta][:group_value].to_s
54
+ x.th(:colspan => @report.columns.size) { x << header }
55
+ else
56
+ @report.columns.each do |column|
57
+
58
+ cell_attrib = {}
59
+ if cst = row[:meta][:cell_styles]
60
+ cst = Item.ensure(cst)
61
+ if cell_styles = cst[column]
62
+ cell_attrib = {:class => cell_styles.join(' ')}
63
+ end
64
+ end
65
+
66
+ x.td(cell_attrib) { x << row[:data][column].to_s }
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+ def cycle(one, two)
76
+ if @current == one
77
+ @current = two
78
+ else
79
+ @current = one
80
+ end
81
+ end
82
+
83
+ def valid?
84
+ @report.is_a? Munger::Report
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,133 @@
1
+ require 'builder'
2
+ module Munger #:nodoc:
3
+ module Render #:nodoc:
4
+ # Render a table that lets the user sort the columns
5
+ class SortableHtml
6
+
7
+ attr_reader :report, :classes
8
+
9
+ # options:
10
+ # :url => /some/url/for/link # link to put on column
11
+ # :params => {:url_params} # parameters from url if any
12
+ # :sort => 'column' # column that is currently sorted
13
+ # :order => 'asc' || 'desc' # order of the currently sorted field
14
+ def initialize(report, options = {})
15
+ @report = report
16
+ @options = options
17
+ # default url and params options
18
+ @options[:url] ||= '/'
19
+ @options[:params] ||= {}
20
+ set_classes(options[:classes])
21
+ end
22
+
23
+ def set_classes(options = nil)
24
+ options = {} if !options
25
+ default = {:table => 'report-table'}
26
+ @classes = default.merge(options)
27
+ end
28
+
29
+ def render
30
+ x = Builder::XmlMarkup.new
31
+
32
+ x.table(:class => @classes[:table]) do
33
+
34
+ x.tr do
35
+ @report.columns.each do |column|
36
+ # TODO: Should be able to see if a column is 'sortable'
37
+ # Assume all columns are sortable here - for now.
38
+ sorted_state = 'unsorted'
39
+ direction = 'asc'
40
+ if [column.to_s, @report.column_data_field(column)].include?(@options[:sort])
41
+ sorted_state = "sorted"
42
+ direction = @options[:order] == 'asc' ? 'desc' : 'asc'
43
+ direction_class = "sorted-#{direction}"
44
+ end
45
+ new_params = @options[:params].merge({'sort' => @report.column_data_field(column),'order' => direction})
46
+ x.th(:class => "columnTitle #{sorted_state} #{direction_class}" ) do
47
+ # x << @report.column_title(column)
48
+ x << "<a href=\"#{@options[:url]}?#{create_querystring(new_params)}\">#{@report.column_title(column)}</a>"
49
+ end
50
+ end
51
+ end
52
+
53
+ @report.process_data.each do |row|
54
+
55
+ classes = []
56
+ classes << row[:meta][:row_styles]
57
+ classes << 'group' + row[:meta][:group].to_s if row[:meta][:group]
58
+ classes << cycle('even', 'odd')
59
+ classes.compact!
60
+
61
+ if row[:meta][:group_header]
62
+ classes << 'groupHeader' + row[:meta][:group_header].to_s
63
+ end
64
+
65
+ row_attrib = {}
66
+ row_attrib = {:class => classes.join(' ')} if classes.size > 0
67
+
68
+ x.tr(row_attrib) do
69
+ if row[:meta][:group_header]
70
+ header = @report.column_title(row[:meta][:group_name]) + ' : ' + row[:meta][:group_value].to_s
71
+ x.th(:colspan => @report.columns.size) { x << header }
72
+ else
73
+ @report.columns.each do |column|
74
+
75
+ cell_attrib = {}
76
+ if cst = row[:meta][:cell_styles]
77
+ cst = Item.ensure(cst)
78
+ if cell_styles = cst[column]
79
+ cell_attrib = {:class => cell_styles.join(' ')}
80
+ end
81
+ end
82
+ # TODO: Clean this up, I don't like it but it's working
83
+ # output the cell
84
+ # x.td(cell_attrib) { x << row[:data][column].to_s }
85
+ x.td(cell_attrib) do
86
+ formatter,*args = *@report.column_formatter(column)
87
+ col_data = row[:data] #[column]
88
+ if formatter && col_data[column]
89
+ formatted = if formatter.class == Proc
90
+ formatter.call(col_data.data)
91
+ elsif col_data[column].respond_to? formatter
92
+ col_data[column].send(formatter, *args)
93
+ elsif
94
+ col_data[column].to_s
95
+ end
96
+ else
97
+ formatted = col_data[column].to_s
98
+ end
99
+ x << formatted.to_s
100
+ end
101
+
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+
110
+ def cycle(one, two)
111
+ if @current == one
112
+ @current = two
113
+ else
114
+ @current = one
115
+ end
116
+ end
117
+
118
+ def valid?
119
+ @report.is_a? Munger::Report
120
+ end
121
+
122
+ private
123
+ def create_querystring(params={})
124
+ qs = []
125
+ params.each do |k,v|
126
+ qs << "#{k}=#{v}"
127
+ end
128
+ qs.join("&")
129
+ end
130
+
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,54 @@
1
+ module Munger #:nodoc:
2
+ module Render #:nodoc:
3
+ class Text
4
+
5
+ attr_reader :report
6
+
7
+ def initialize(report)
8
+ @report = report
9
+ end
10
+
11
+ def render
12
+ output = ''
13
+ depth = {}
14
+
15
+ # find depth
16
+ @report.process_data.each do |row|
17
+ @report.columns.each do |column|
18
+ i = row[:data][column].to_s.size
19
+ depth[column] ||= @report.column_title(column).to_s.size
20
+ depth[column] = (depth[column] < i) ? i : depth[column]
21
+ end
22
+ end
23
+
24
+ # header
25
+ output += '|'
26
+ @report.columns.each do |column|
27
+ output += @report.column_title(column).to_s.ljust(depth[column] + 1) + '| '
28
+ end
29
+ output += "\n"
30
+
31
+ total = depth.values.inject { |sum, i| sum + i } + (depth.size * 3)
32
+ 0.upto(total) { |i| output += '-' }
33
+ output += "\n"
34
+
35
+ # body
36
+ @report.process_data.each do |row|
37
+ (row[:meta][:group]) ? sep = ':' : sep = '|'
38
+ output += sep
39
+ @report.columns.each do |column|
40
+ output += row[:data][column].to_s.ljust(depth[column] + 1) + sep + ' '
41
+ end
42
+ output += "\n"
43
+ end
44
+
45
+ output
46
+ end
47
+
48
+ def valid?
49
+ @report.is_a? Munger::Report
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,349 @@
1
+ module Munger #:nodoc:
2
+
3
+ class Report
4
+
5
+ attr_writer :data, :sort, :columns, :subgroup, :subgroup_options, :aggregate
6
+ attr_accessor :column_titles, :column_data_fields, :column_formatters
7
+ attr_reader :process_data, :grouping_level
8
+
9
+ # r = Munger::Report.new ( :data => data,
10
+ # :columns => [:collect_date, :spot_name, :airings, :display_name],
11
+ # :sort => [:collect_date, :spot_name]
12
+ # :subgroup => @group_list,
13
+ # :aggregate => {:sum => new_columns} )
14
+ # report = r.highlight
15
+ def initialize(options = {})
16
+ @grouping_level = 0
17
+ @column_titles = {}
18
+ @column_data_fields = {}
19
+ @column_formatters = {}
20
+ set_options(options)
21
+ end
22
+
23
+ def self.from_data(data)
24
+ Report.new(:data => data)
25
+ end
26
+
27
+ def set_options(options)
28
+ if d = options[:data]
29
+ if d.is_a? Munger::Data
30
+ @data = d
31
+ else
32
+ @data = Munger::Data.new(:data => d)
33
+ end
34
+ end
35
+ self.sort(options[:sort]) if options[:sort]
36
+ self.columns(options[:columns]) if options[:columns]
37
+ self.subgroup(options[:subgroup]) if options[:subgroup]
38
+ self.aggregate(options[:aggregate]) if options[:aggregate]
39
+ end
40
+
41
+ def processed?
42
+ if @process_data
43
+ true
44
+ else
45
+ false
46
+ end
47
+ end
48
+
49
+ # returns ReportTable
50
+ def process(options = {})
51
+ set_options(options)
52
+
53
+ # sorts and fills NativeReport
54
+ @report = translate_native(do_field_sort(@data.data))
55
+
56
+ do_add_groupings
57
+ do_add_aggregate_rows
58
+
59
+ self
60
+ end
61
+
62
+ def sort(values = nil)
63
+ if values
64
+ @sort = values
65
+ self
66
+ else
67
+ @sort
68
+ end
69
+ end
70
+
71
+ def subgroup(values = nil, options = {})
72
+ if values
73
+ @subgroup = values
74
+ @subgroup_options = options
75
+ self
76
+ else
77
+ @subgroup
78
+ end
79
+ end
80
+
81
+ def columns(values = nil)
82
+ if values
83
+ if values.is_a? Hash
84
+ @columns = values.keys
85
+ @column_titles = values
86
+ else
87
+ @columns = Data.array(values)
88
+ end
89
+ self
90
+ else
91
+ @columns ||= @data.columns
92
+ end
93
+ end
94
+
95
+ def column_title(column)
96
+ if c = @column_titles[column]
97
+ return c.to_s
98
+ else
99
+ return column.to_s
100
+ end
101
+ end
102
+
103
+ def column_data_field(column)
104
+ @column_data_fields[column] || column.to_s
105
+ end
106
+
107
+ def column_formatter(column)
108
+ @column_formatters[column]
109
+ end
110
+
111
+ def aggregate(values = nil)
112
+ if values
113
+ @aggregate = values
114
+ self
115
+ else
116
+ @aggregate
117
+ end
118
+ end
119
+
120
+ def rows
121
+ @process_data.size
122
+ end
123
+
124
+ def valid?
125
+ (@data.is_a? Munger::Data) && (@data.valid?)
126
+ end
127
+
128
+ # @report.style_cells('highlight') { |cell, row| cell > 32 }
129
+ def style_cells(style, options = {})
130
+ @process_data.each_with_index do |row, index|
131
+
132
+ # filter columns to look at
133
+ if options[:only]
134
+ cols = Data.array(options[:only])
135
+ elsif options [:except]
136
+ cols = columns - Data.array(options[:except])
137
+ else
138
+ cols = columns
139
+ end
140
+
141
+ if options[:no_groups] && row[:meta][:group]
142
+ next
143
+ end
144
+
145
+ cols.each do |col|
146
+ if yield(row[:data][col], row[:data])
147
+ @process_data[index][:meta][:cell_styles] ||= {}
148
+ @process_data[index][:meta][:cell_styles][col] ||= []
149
+ @process_data[index][:meta][:cell_styles][col] << style
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ # @report.style_rows('highlight') { |row| row.age > 32 }
156
+ def style_rows(style, options = {})
157
+ @process_data.each_with_index do |row, index|
158
+ if yield(row[:data])
159
+ @process_data[index][:meta][:row_styles] ||= []
160
+ @process_data[index][:meta][:row_styles] << style
161
+ end
162
+ end
163
+ end
164
+
165
+ # post-processing calls
166
+
167
+ def get_subgroup_rows(group_level = nil)
168
+ data = @process_data.select { |r| r[:meta][:group] }
169
+ data = data.select { |r| r[:meta][:group] == group_level } if group_level
170
+ data
171
+ end
172
+
173
+ def to_s
174
+ pp @process_data
175
+ end
176
+
177
+ private
178
+
179
+ def translate_native(array_of_hashes)
180
+ @process_data = []
181
+ array_of_hashes.each do |row|
182
+ @process_data << {:data => Item.ensure(row), :meta => {:data => true}}
183
+ end
184
+ end
185
+
186
+ def do_add_aggregate_rows
187
+ return false if !@aggregate
188
+ return false if !@aggregate.is_a? Hash
189
+
190
+ totals = {}
191
+
192
+ @process_data.each_with_index do |row, index|
193
+ if row[:meta][:data]
194
+ @aggregate.each do |type, columns|
195
+ Data.array(columns).each do |column|
196
+ value = row[:data][column]
197
+ @grouping_level.downto(0) do |level|
198
+ totals[column] ||= {}
199
+ totals[column][level] ||= []
200
+ totals[column][level] << value
201
+ end
202
+ end
203
+ end
204
+ elsif level = row[:meta][:group]
205
+ # write the totals and reset level
206
+ @aggregate.each do |type, columns|
207
+ Data.array(columns).each do |column|
208
+ data = totals[column][level]
209
+ @process_data[index][:data][column] = calculate_aggregate(type, data)
210
+ totals[column][level] = []
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ total_row = {:data => {}, :meta => {:group => 0}}
217
+ # write one row at the end with the totals
218
+ @aggregate.each do |type, columns|
219
+ Data.array(columns).each do |column|
220
+ data = totals[column][0]
221
+ total_row[:data][column] = calculate_aggregate(type, data)
222
+ end
223
+ end
224
+ @process_data << total_row
225
+
226
+ end
227
+
228
+ def calculate_aggregate(type, data)
229
+ return 0 if !data
230
+ if type.is_a? Proc
231
+ type.call(data)
232
+ else
233
+ case type
234
+ when :count
235
+ data.size
236
+ when :average
237
+ sum = data.inject {|sum, n| sum + n }
238
+ (sum / data.size) rescue 0
239
+ else
240
+ data.inject {|sum, n| sum + n }
241
+ end
242
+ end
243
+ end
244
+
245
+ def do_add_groupings
246
+ return false if !@subgroup
247
+ sub = Data.array(@subgroup)
248
+ @grouping_level = sub.size
249
+
250
+ current = {}
251
+ new_data = []
252
+
253
+ first_row = @process_data.first
254
+ sub.reverse.each do |group|
255
+ current[group] = first_row[:data][group]
256
+ end
257
+ prev_row = {:data => {}}
258
+
259
+ @process_data.each_with_index do |row, index|
260
+ # insert header title rows
261
+ next_row = @process_data[index + 1]
262
+
263
+ if next_row
264
+
265
+ # insert header rows
266
+ if @subgroup_options[:with_headers]
267
+ level = 1
268
+ sub.each do |group|
269
+ if (prev_row[:data][group] != current[group]) && current[group]
270
+ group_row = {:data => {}, :meta => {:group_header => level,
271
+ :group_name => group, :group_value => row[:data][group]}}
272
+ new_data << group_row
273
+ end
274
+ level += 1
275
+ end
276
+ end
277
+
278
+ # insert current row
279
+ new_data << row
280
+
281
+
282
+ # insert footer rows
283
+ level = @grouping_level
284
+ sub.reverse.each do |group|
285
+ if (next_row[:data][group] != current[group]) && current[group]
286
+ group_row = {:data => {}, :meta => {:group => level, :group_name => group}}
287
+ new_data << group_row
288
+ end
289
+ current[group] = next_row[:data][group]
290
+ level -= 1
291
+ end
292
+
293
+ prev_row = row
294
+
295
+ else # last row
296
+ level = @grouping_level
297
+
298
+ # insert header rows
299
+ sub.each do |group|
300
+ if (prev_row[:data][group] != current[group]) && current[group]
301
+ group_row = {:data => {}, :meta => {:group_header => level,
302
+ :group_name => group, :group_value => row[:data][group]}}
303
+ new_data << group_row
304
+ end
305
+ end
306
+
307
+ new_data << row
308
+
309
+ sub.reverse.each do |group|
310
+ group_row = {:data => {}, :meta => {:group => level, :group_name => group}}
311
+ new_data << group_row
312
+ level -= 1
313
+ end
314
+ end
315
+ end
316
+
317
+ @process_data = new_data
318
+ end
319
+
320
+ def do_field_sort(data)
321
+ data.sort do |a, b|
322
+ compare = 0
323
+ a = Item.ensure(a)
324
+ b = Item.ensure(b)
325
+
326
+ Data.array(@sort).each do |sorting|
327
+ if sorting.is_a?(String) || sorting.is_a?(Symbol)
328
+ compare = a[sorting.to_s] <=> b[sorting.to_s] rescue 0
329
+ break if compare != 0
330
+ elsif sorting.is_a? Array
331
+ key = sorting[0]
332
+ func = sorting[1]
333
+ if func == :asc
334
+ compare = a[key] <=> b[key]
335
+ elsif func == :desc
336
+ compare = b[key] <=> a[key]
337
+ elsif func.is_a? Proc
338
+ compare = func.call(a[key], b[key])
339
+ end
340
+ break if compare != 0
341
+ end
342
+ end
343
+ compare
344
+ end
345
+ end
346
+
347
+ end
348
+
349
+ end