ruport 0.4.13 → 0.4.15

Sign up to get free protection for your applications and to get access to all the features.
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)