kbaum-munger 0.1.4

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.
@@ -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