ruport 0.4.13 → 0.4.15

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/CHANGELOG CHANGED
@@ -1,4 +1,26 @@
1
- The current version of Ruby Reports is 0.4.13
1
+ The current version of Ruby Reports is 0.4.15
2
+
3
+ changes since 0.4.13
4
+
5
+ - Began building a new data handling system, which is much faster than
6
+ DataSet/DataRow and takes up less memory. It is also very young, though.
7
+ (See Data::Table and Data::Record)
8
+
9
+ - Added init_plugin hooks
10
+
11
+ - Added DataSet#rename_columns
12
+
13
+ - Added taggable mixin
14
+
15
+ - Added an options accessor to help solve ticket #8
16
+
17
+ - Alioth now has a powerful hook back into the engine from plugins via helpers
18
+
19
+ - added action / attribute methods for format engine / plugin via MetaTools
20
+
21
+ - added accessor style methods to DataRow via method_missing
22
+
23
+ - added prune method for Table formmatting.
2
24
 
3
25
  changes since 0.4.11
4
26
 
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ end
21
21
 
22
22
  spec = Gem::Specification.new do |spec|
23
23
  spec.name = LEAN ? "lean-ruport" : "ruport"
24
- spec.version = "0.4.13"
24
+ spec.version = "0.4.15"
25
25
  spec.platform = Gem::Platform::RUBY
26
26
  spec.summary = "A generalized Ruby report generation and templating engine."
27
27
  spec.files = Dir.glob("{examples,lib,test}/**/**/*") +
data/TODO CHANGED
@@ -6,8 +6,6 @@ Immediate Goals:
6
6
 
7
7
  - PDF document support + example
8
8
 
9
- - event system
10
-
11
9
  - Composite key selection
12
10
 
13
11
  - Value modification predicate.
@@ -18,6 +16,4 @@ Immediate Goals:
18
16
 
19
17
  - Document the inner classes of Format
20
18
 
21
- - make the Fetchable module to abstract data acquisition
22
-
23
19
  - Calculated fields
@@ -0,0 +1,20 @@
1
+ module Ruport::Data
2
+ class Collection
3
+ require "forwardable"
4
+ extend Forwardable
5
+ include Enumerable
6
+ include Taggable
7
+
8
+ def initialize(data=nil,options={})
9
+ @data = data.dup if data
10
+ end
11
+
12
+ def as(type)
13
+ Ruport::Format.table :data => self, :plugin => type
14
+ end
15
+
16
+ attr_reader :data
17
+ def_delegators :@data, :each, :length, :[], :empty?
18
+ end
19
+ end
20
+
@@ -0,0 +1,81 @@
1
+ module Ruport::Data
2
+ class Record
3
+ require "forwardable"
4
+ extend Forwardable
5
+ include Enumerable
6
+ include Taggable
7
+
8
+ def initialize(data,options={})
9
+ @data = data.dup
10
+ @attributes = options[:attributes]
11
+ end
12
+
13
+ attr_reader :data
14
+ def_delegators :@data,:each, :length
15
+
16
+
17
+ def [](index)
18
+ if index.kind_of? Integer
19
+ @data[index]
20
+ else
21
+ @data[@attributes.index(index)]
22
+ end
23
+ end
24
+
25
+ def []=(index, value)
26
+ if index.kind_of? Integer
27
+ @data[index] = value
28
+ else
29
+ @data[attributes.index(index)] = value
30
+ end
31
+ end
32
+
33
+ def ==(other)
34
+ return false if attributes && !other.attributes
35
+ return false if other.attributes && !attributes
36
+ (attributes == other.attributes) && (data == other.data)
37
+ end
38
+
39
+ alias_method :eql?, :==
40
+
41
+ def to_a; data.dup; end
42
+
43
+ def to_h; Hash[*attributes.zip(data).flatten] end
44
+
45
+ def attributes; @attributes && @attributes.dup; end
46
+
47
+ def attributes=(a); @attributes=a; end
48
+
49
+ def reorder(*indices)
50
+ dup.reorder! *indices
51
+ end
52
+
53
+ def reorder!(*indices)
54
+ @data = indices.map { |i| self[i] }
55
+ if attributes
56
+ if indices.all? { |e| e.kind_of? Integer }
57
+ @attributes = indices.map { |i| @attributes[i] }
58
+ else
59
+ @attributes = indices
60
+ end
61
+ end; self
62
+ end
63
+
64
+ def dup
65
+ self.class.new(data,:attributes => attributes)
66
+ end
67
+
68
+ #FIXME: This does not take into account frozen / tainted state
69
+ alias_method :clone, :dup
70
+
71
+ def method_missing(id,*args)
72
+ id = id.to_s.gsub(/=$/,"")
73
+ if @attributes.include?(id)
74
+ args.empty? ? self[id] : self[id] = args.first
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,57 @@
1
+ module Ruport::Data
2
+ class Table < Collection
3
+ def initialize(options={})
4
+ @column_names = options[:column_names].dup if options[:column_names]
5
+ @data = []
6
+ options[:data].each { |e| self << e } if options[:data]
7
+ end
8
+
9
+ attr_reader :column_names
10
+
11
+ def column_names=(other)
12
+ @column_names = other.dup
13
+ end
14
+
15
+ def <<(other)
16
+ case(other)
17
+ when Array
18
+ @data << Record.new(other, :attributes => @column_names)
19
+ when Hash
20
+ raise unless @column_names
21
+ arr = @column_names.map { |k| other[k] }
22
+ @data << Record.new(arr, :attributes => @column_names)
23
+ when Record
24
+ @data << Record.reorder(*column_names)
25
+ end
26
+ end
27
+
28
+ def reorder!(*indices)
29
+ @column_names = indices
30
+ @data.each { |r| r.reorder! *indices }; self
31
+ end
32
+
33
+ def reorder(*indices)
34
+ dup.reorder! *indices
35
+ end
36
+
37
+ def self.load(csv_file, options = {})
38
+ options = {:has_names => true}.merge(options)
39
+ require "fastercsv"
40
+ loaded_data = self.new
41
+
42
+ first_line = true
43
+ FasterCSV.foreach(csv_file) do |row|
44
+ if first_line && options[:has_names]
45
+ loaded_data.column_names = row
46
+ first_line = false
47
+ elsif !block_given?
48
+ loaded_data << row
49
+ else
50
+ yield(loaded_data,row)
51
+ end
52
+ end
53
+ return loaded_data
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ module Ruport::Data
2
+
3
+ module Taggable
4
+
5
+ def tag(tag_name)
6
+ tags << tag_name unless has_tag? tag_name
7
+ end
8
+
9
+ def delete_tag(tag_name)
10
+ tags.delete tag_name
11
+ end
12
+
13
+ def has_tag?(tag_name)
14
+ tags.include? tag_name
15
+ end
16
+
17
+ def tags
18
+ @ruport_tags ||= []
19
+ end
20
+
21
+ def tags=(tags_list)
22
+ @ruport_tags = tags_list
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1 @@
1
+ %w[taggable record collection table].each { |l| require "ruport/data/#{l}" }
@@ -74,7 +74,7 @@ module Ruport
74
74
 
75
75
  attr_accessor :fields, :tags
76
76
  alias_method :column_names, :fields
77
-
77
+ alias_method :attributes, :fields
78
78
  # Returns a new DataRow
79
79
  def +(other)
80
80
  DataRow.new @fields + other.fields, :data => (@data + other.to_a)
@@ -177,5 +177,11 @@ module Ruport
177
177
  :status => :fatal, :exception => ArgumentError, :level => :log_only
178
178
  end
179
179
  end
180
- end
180
+
181
+ def method_missing(id,*args)
182
+ f = id.to_s.gsub(/=$/,'')
183
+ return super unless fields.include?(f)
184
+ args.empty? ? self[f] : self[f] = args[0]
185
+ end
186
+ end
181
187
  end
@@ -234,6 +234,14 @@ module Ruport
234
234
  @fields = d.fields
235
235
  end
236
236
 
237
+ def rename_columns(cols)
238
+ if cols.kind_of?(Array)
239
+ cols.each_with_index { |c,i| fields[i] = c }
240
+ else
241
+ cols.map { |k,v| fields[fields.index(k)] = v }
242
+ end
243
+ @data.each { |r| r.fields = fields }
244
+ end
237
245
  # uses Format::Builder to render DataSets in various ready to output
238
246
  # formats.
239
247
  #
@@ -249,7 +257,7 @@ module Ruport
249
257
  #
250
258
  # This will enable <tt>data.as(:my_format_name)</tt>
251
259
  def as(format,&action)
252
- t = Format.table_object(:data => clone, :plugin => format)
260
+ t = Format.table_object(:data => self, :plugin => format)
253
261
  action.call(t) if block_given?
254
262
  t.render
255
263
  end
@@ -286,6 +294,7 @@ module Ruport
286
294
  f.all? { |e| e.kind_of? Integer } &&
287
295
  f.inject([]) { |s,e| s + [@fields[e]] } || f
288
296
  end
297
+
289
298
 
290
299
  end
291
300
  end
@@ -1,23 +1,25 @@
1
1
  module Ruport
2
2
  class Format::Engine
3
3
  require "forwardable"
4
-
5
4
  class << self
6
5
 
7
6
  include Enumerable
7
+ include MetaTools
8
8
  extend Forwardable
9
9
 
10
- def_delegator :@data, :each
11
-
12
- def renderer(&block)
13
- block = lambda { data } unless block_given?
14
- (class << self; self; end).send(:define_method, :render, &block)
15
- end
16
-
17
10
  attr_accessor :engine_klasses
18
11
  attr_reader :plugin
19
12
  attr_reader :data
20
13
  attr_accessor :klass_binding
14
+ attr_reader :options
15
+
16
+ def_delegator :@data, :each
17
+ private :attribute, :attributes, :singleton, :action
18
+
19
+ def renderer(&block)
20
+ block = lambda { data } unless block_given?
21
+ singleton.send(:define_method, :render,&block)
22
+ end
21
23
 
22
24
  def alias_engine(klass,name)
23
25
  Format::Engine.engine_klasses ||= {}
@@ -28,10 +30,15 @@ module Ruport
28
30
  @data = data
29
31
  active_plugin.data = data.dup if active_plugin
30
32
  end
33
+
34
+ def options=(opts)
35
+ @options = opts
36
+ active_plugin.options = options if active_plugin
37
+ end
31
38
 
32
39
  def active_plugin
40
+ return yield(@format_plugins[:current]) if block_given?
33
41
  @format_plugins[:current]
34
- #plugin && @format_plugins[plugin]
35
42
  end
36
43
 
37
44
  def plugin=(p)
@@ -49,6 +56,9 @@ module Ruport
49
56
  raise "No plugin specified" unless plugin
50
57
  raise "No data provided" unless data
51
58
  active_plugin.data = data.dup
59
+ if active_plugin.respond_to? :init_plugin_helper
60
+ active_plugin.init_plugin_helper(self)
61
+ end
52
62
  end
53
63
 
54
64
  def flush_data
@@ -73,6 +83,11 @@ module Ruport
73
83
  format_plugins.values
74
84
  end
75
85
 
86
+ def method_missing(id)
87
+ super unless active_plugin.respond_to?("#{id}_helper")
88
+ return active_plugin.send("#{id}_helper",self)
89
+ end
90
+
76
91
  end
77
92
  end
78
93
 
@@ -81,8 +96,8 @@ module Ruport
81
96
  renderer do
82
97
  super
83
98
  active_plugin.rendered_field_names = ""
84
- build_field_names if (data.respond_to?(:fields) &&
85
- data.fields && show_field_names)
99
+ build_field_names if (data.respond_to?(:column_names) &&
100
+ data.column_names && show_field_names)
86
101
  a = active_plugin.render_table
87
102
  end
88
103
 
@@ -96,6 +111,17 @@ module Ruport
96
111
  data[0].to_a.length
97
112
  end
98
113
 
114
+ def prune(limit=data[0].length)
115
+ (0...limit).each do |field|
116
+ last = ""
117
+ data.each_with_index { |e,i|
118
+ next if i.zero? || field.nonzero? && data[i][field-1]
119
+ last = data[i-1][field] if data[i-1][field]
120
+ data[i][field] = nil if e[field] == last
121
+ }
122
+ end
123
+ end
124
+
99
125
  attr_accessor :show_field_names
100
126
 
101
127
  private
@@ -105,7 +131,7 @@ module Ruport
105
131
  active_plugin.rendered_field_names = active_plugin.build_field_names
106
132
  end
107
133
  end
108
-
134
+
109
135
  end
110
136
 
111
137
  alias_engine Table, :table_engine
@@ -4,10 +4,17 @@ module Ruport
4
4
  class << self
5
5
 
6
6
  attr_accessor :data
7
+ attr_accessor :options
8
+
9
+ include MetaTools
7
10
 
8
- def plugin_name(name)
9
- @name = name
11
+ def helper(name,&block)
12
+ singleton.send( :define_method, "#{name}_helper", &block )
10
13
  end
14
+
15
+ private :singleton, :attribute, :attributes, :action
16
+
17
+ def plugin_name(name=nil); @name ||= name; end
11
18
 
12
19
  def format_name
13
20
  pattern = /Ruport::Format|Plugin/
@@ -18,11 +25,11 @@ module Ruport
18
25
  def renderer(render_type,&block)
19
26
  m = "render_#{render_type}".to_sym
20
27
  block = lambda { data } unless block_given?
21
- (class << self; self; end).send(:define_method, m, &block)
28
+ singleton.send(:define_method, m, &block)
22
29
  end
23
30
 
24
31
  def format_field_names(&block)
25
- (class << self; self; end).send(:define_method, :build_field_names, &block)
32
+ singleton.send( :define_method, :build_field_names, &block)
26
33
  end
27
34
 
28
35
  def register_on(klass)
@@ -39,7 +46,7 @@ module Ruport
39
46
  @options.merge!(hash)
40
47
  @options.dup
41
48
  end
42
-
49
+
43
50
  attr_accessor :rendered_field_names
44
51
  attr_accessor :pre, :post
45
52
  attr_accessor :header, :footer
@@ -47,14 +54,14 @@ module Ruport
47
54
 
48
55
 
49
56
  class CSVPlugin < Format::Plugin
50
-
57
+
58
+ helper(:init_plugin) { require "fastercsv" }
59
+
51
60
  format_field_names do
52
- require "fastercsv"
53
- FasterCSV.generate { |csv| csv << data.fields }
61
+ FasterCSV.generate { |csv| csv << data.column_names }
54
62
  end
55
63
 
56
64
  renderer :table do
57
- require "fastercsv"
58
65
  rendered_field_names +
59
66
  FasterCSV.generate { |csv| data.each { |r| csv << r } }
60
67
  end
@@ -78,7 +85,7 @@ module Ruport
78
85
  }
79
86
  }
80
87
 
81
- a = data.inject(th){ |s,r|
88
+ a = data.inject(th){ |s,r|
82
89
  s + "| #{r.to_a.join(' | ')} |\n"
83
90
  } << hr
84
91
 
@@ -88,39 +95,40 @@ module Ruport
88
95
  r.gsub!(/\A.{#{width},}/) { |m| m[0,width-2] += ">>" }
89
96
  }.join("\n") << "\n"
90
97
  end
98
+
91
99
  format_field_names do
92
- data.fields.each_with_index { |f,i|
93
- data.fields[i] = f.to_s.center(max_col_width(i))
100
+ data.column_names.each_with_index { |f,i|
101
+ data.column_names[i] = f.to_s.center(max_col_width(i))
94
102
  }
95
- "#{hr}| #{data.fields.to_a.join(' | ')} |\n"
103
+ "#{hr}| #{data.column_names.to_a.join(' | ')} |\n"
96
104
  end
97
105
 
98
- def self.max_col_width(index)
99
- f = data.fields if data.respond_to? :fields
106
+ action :max_col_width do |index|
107
+ f = data.column_names if data.respond_to? :column_names
100
108
  d = DataSet.new f, :data => data
101
109
 
102
110
  cw = d.map { |r| r[index].to_s.length }.max
103
111
 
104
- return cw unless d.fields
112
+ return cw unless d.column_names
105
113
 
106
- nw = (index.kind_of?(Integer) ? d.fields[index] : index ).to_s.length
114
+ nw = (index.kind_of?(Integer) ? d.column_names[index] : index ).to_s.length
107
115
 
108
116
  [cw,nw].max
109
117
  end
110
118
 
111
- def self.table_width
112
- f = data.fields if data.respond_to? :fields
119
+ action :table_width do
120
+ f = data.column_names if data.respond_to? :column_names
113
121
  d = DataSet.new f, :data => data
114
122
 
115
- d[0].fields.inject(0) { |s,e| s + max_col_width(e) }
123
+ d[0].attributes.inject(0) { |s,e| s + max_col_width(e) }
116
124
  end
117
125
 
118
- def self.hr
126
+ action :hr do
119
127
  len = data[0].to_a.length * 3 + table_width + 1
120
128
  "+" + "-"*(len-2) + "+\n"
121
129
  end
122
130
 
123
- class << self; attr_accessor :right_margin; end
131
+ attribute :right_margin
124
132
 
125
133
  register_on :table_engine
126
134
  register_on :document_engine
@@ -145,7 +153,7 @@ module Ruport
145
153
  pdf.render
146
154
  end
147
155
 
148
- format_field_names { data.fields }
156
+ format_field_names { data.column_names }
149
157
 
150
158
  register_on :table_engine
151
159
  end
@@ -165,7 +173,7 @@ module Ruport
165
173
  end
166
174
 
167
175
  format_field_names do
168
- s = "|_." + data.fields.join(" |_.") + "|\n"
176
+ s = "|_." + data.column_names.join(" |_.") + "|\n"
169
177
  end
170
178
 
171
179
  register_on :table_engine
@@ -0,0 +1,19 @@
1
+ module Ruport
2
+ module MetaTools
3
+ def singleton; (class << self; self; end); end
4
+
5
+ def attribute(sym,value = nil)
6
+ singleton.send(:attr_accessor, sym )
7
+ self.send("#{sym}=",value)
8
+ end
9
+
10
+ def attributes(syms)
11
+ syms.each { |s| attribute s }
12
+ end
13
+
14
+ def action(name,&block)
15
+ singleton.send(:define_method, name, &block)
16
+ end
17
+ end
18
+ end
19
+
@@ -1,8 +1,8 @@
1
1
  module Ruport
2
2
  module Reportable
3
- def formatted_table(type,options={},&block)
3
+ def formatted_table(type,options={})
4
4
  to_ds(:find => options[:find],:columns => options[:columns]).as(type){ |e|
5
- block[e] if block_given?
5
+ yield(e) if block_given?
6
6
  }
7
7
  end
8
8
  def to_ds(options={})
data/lib/ruport/report.rb CHANGED
@@ -137,7 +137,7 @@ module Ruport
137
137
  # result = query "select * from foo", :as => :pdf
138
138
  #
139
139
  # See source of this function and methods of Ruport::Query for details.
140
- def query(sql, options={}, &action)
140
+ def query(sql, options={})
141
141
  options[:origin] ||= :string
142
142
  options[:source] ||= @source
143
143
 
@@ -147,7 +147,7 @@ module Ruport
147
147
  elsif options[:as]
148
148
  Format.table :data => q.result, :plugin => options[:as]
149
149
  else
150
- block_given? ? action.call(q.result) : q.result
150
+ block_given? ? yield(q.result) : q.result
151
151
  end
152
152
  end
153
153
 
data/lib/ruport.rb CHANGED
@@ -14,7 +14,7 @@ module Ruport
14
14
 
15
15
  begin; require 'rubygems'; rescue LoadError; nil end
16
16
 
17
- VERSION = "Ruby Reports Version 0.4.13"
17
+ VERSION = "Ruby Reports Version 0.4.15"
18
18
 
19
19
  # Ruports logging and error interface.
20
20
  # Can generate warnings or raise fatal errors
@@ -57,6 +57,6 @@ module Ruport
57
57
  end
58
58
 
59
59
 
60
- %w[config report format query data_row data_set].each { |lib|
60
+ %w[config meta_tools report format query data_row data_set data].each { |lib|
61
61
  require "ruport/#{lib}"
62
62
  }
data/test/tc_data_row.rb CHANGED
@@ -21,6 +21,14 @@ class TestDataRow < Test::Unit::TestCase
21
21
  assert_equal("[1,2]",@rows[0].to_s)
22
22
  end
23
23
 
24
+ def test_accessor_style
25
+ row = @rows[0]
26
+ assert_equal 1, row.foo
27
+ assert_equal 2, row.bar
28
+ row.bar = 17
29
+ assert_equal [1,17], row.to_a
30
+ end
31
+
24
32
  def test_tagging
25
33
  @rows[0].tag_as :foo
26
34
  assert_equal(true, @rows[0].has_tag?(:foo) )
data/test/tc_data_set.rb CHANGED
@@ -46,6 +46,23 @@ class TestDataSet < Test::Unit::TestCase
46
46
  @data[2].to_a )
47
47
  end
48
48
 
49
+ def test_rename_columns
50
+ @data.rename_columns "col1" => "Column 1",
51
+ "col2" => "Column 2",
52
+ "col3" => "Column 3"
53
+ assert_equal %w[Column\ 1 Column\ 2 Column\ 3], @data.fields
54
+ @data.each do |r|
55
+ assert_equal %w[Column\ 1 Column\ 2 Column\ 3], r.fields
56
+ end
57
+ assert_equal "b", @data[0]["Column 2"]
58
+ assert_equal "e", @data[1]["Column 3"]
59
+ @data.rename_columns %w[one two three]
60
+ assert_equal %w[one two three], @data.fields
61
+ @data.each do |r|
62
+ assert_equal %w[one two three], r.fields
63
+ end
64
+ end
65
+
49
66
  def test_delete_if
50
67
  @data.delete_if { |r| r.any? { |e| e.empty? } }
51
68
  assert_equal([%w[a b c]],@data.to_a)