odf-report 0.1.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest CHANGED
@@ -1,7 +1,12 @@
1
1
  lib/odf-report.rb
2
+ lib/odf-report/report.rb
3
+ lib/odf-report/table.rb
4
+ lib/odf-report/section.rb
5
+ lib/odf-report/file_ops.rb
6
+ lib/odf-report/hash_gsub.rb
2
7
  odf-report.gemspec
3
- Rakefile
4
8
  README.textile
5
9
  test/test.odt
10
+ test/sections.odt
6
11
  test/test.rb
7
12
  Manifest
data/README.textile CHANGED
@@ -1,37 +1,35 @@
1
1
  h1. ODF-REPORT
2
2
 
3
- Gem for generating .odt files by making strings, images and tables substitutions in a previously created .odt file.
3
+ Gem for generating .odt files by making strings, images, tables and sections replacements in a previously created .odt file.
4
4
 
5
- <hr/>
6
5
 
7
- h3. INSTALL
6
+ h2. INSTALL
8
7
 
9
- @gem install sandrods-odf-report --source=http://gems.github.com@
8
+ (sudo) gem install odf-report
10
9
 
11
- <hr/>
12
10
 
13
- h3. USAGE
11
+ h2. USAGE
14
12
 
15
- h4. Step 1 -- the template
13
+ h3. Step 1 -- the template
16
14
 
17
15
  First of all, you need to create a .odt file to serve as a template
18
16
 
19
17
  Templates are normal .odt files with placeholders for Substitutions
20
18
 
21
- There are now three kinds of substitutions available: *fields*, *tables* and *images*.
19
+ There are now *four* kinds of substitutions available: *fields*, *tables*, *images* and *sections*.
22
20
 
23
- h4. Fields placeholders
21
+ h3. Fields placeholders
24
22
 
25
23
  It's just an upcase sentence, surrounded by brackets. It will be replaced for wathever value you supply.
26
24
 
27
25
  In the folowing example:
28
26
 
29
27
  <pre>
30
- report = ODFReport.new("Users/john/my_template.odt") do |r|
28
+ report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
31
29
 
32
30
  r.add_field :user_name, @user.name
33
31
  r.add_field :address, "My new address"
34
-
32
+
35
33
  end
36
34
  </pre>
37
35
 
@@ -39,25 +37,26 @@ All occurences of @[USER_NAME]@ found in the file will be replaced by the value
39
37
 
40
38
  It's as simple as that.
41
39
 
42
- h4. Table placeholders
40
+
41
+ h3. Table placeholders
43
42
 
44
43
  To use table placeholders, you should create a Table in your document and give it a name. In OpenOffice, it's just a matter of right-clicking the table you just created, choose _Table Properties..._ and type a name in the Name field.
45
44
 
46
- If the table has two rows, the first one will be treated as a *header* and left untouched. Otherwise you should use a table with one row only.
45
+ If you inform @:header=>true@, the first row will be treated as a *header* and left untouched. The remaining rows will be used as the template for the table. If you have more than one template row, they will be cycled. This is usefull for making zebra tables.
47
46
 
48
47
  As with Field placeholders, just insert a @[FIELD_NAME]@ in each cell and let the magic takes place.
49
48
 
50
49
  Taking the folowing example:
51
50
 
52
51
  <pre>
53
- report = ODFReport.new("Users/john/my_template.odt") do |r|
52
+ report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
54
53
 
55
54
  r.add_field "USER_NAME", @user.nome
56
55
  r.add_field "ADDRESS", @user.address
57
56
 
58
- r.add_table("TABLE_1", @list_of_itens) do |row, item|
59
- row["ITEM_ID"] = item.id
60
- row["DESCRIPTION"] = "==> #{item.description}"
57
+ r.add_table("TABLE_1", @list_of_itens, :header=>true) do |t|
58
+ t.add_column(:item_id, :id)
59
+ t.add_column(:description) do { |item| "==> #{item.description}" }
61
60
  end
62
61
 
63
62
  end
@@ -70,9 +69,9 @@ and considering you have a table like this in your template
70
69
  | [ITEM_ID] | [DESCRIPTION] |
71
70
  ---------------------------------
72
71
 
73
- * this is my lame attempt to draw a table.
74
- you don't suppose to type this.
75
- you have to use an actual table.
72
+ * this is my lame attempt to draw a table.
73
+ you don't suppose to type this.
74
+ you have to use an actual table.
76
75
  i don't know... just thought I'd mention it ;-)
77
76
  </pre>
78
77
 
@@ -80,7 +79,8 @@ and a collection @list_of_itens, it will be created one row for each item in the
80
79
 
81
80
  Any format applied to the fields in the template will be preserved.
82
81
 
83
- h4. Images
82
+
83
+ h3. Images
84
84
 
85
85
  You must put a mock image in your odt template and give it a name. That name will be used to replace the mock image for the actual image.
86
86
  You can also assign any properties you want to the mock image and they will be kept once the image is replaced.
@@ -88,61 +88,99 @@ You can also assign any properties you want to the mock image and they will be k
88
88
  An image replace would look like this:
89
89
 
90
90
  <pre>
91
- report = ODFReport.new("Users/john/my_template.odt") do |r|
91
+ report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
92
92
 
93
93
  r.add_image :graphics1, "/path/to/the/image.jpg"
94
94
 
95
95
  end
96
96
  </pre>
97
97
 
98
- And that's it.
98
+
99
+ h3. Sections (NEW!)
100
+
101
+ Sometimes, you have to repeat a whole chunk of a document, in a structure a lot more complex than a table. Now you can make a Section in your template and use it in this situations. Creating a Session in OpenOffice is as easy as select menu *Insert* and then *Section...*, and then choose a name for it.
102
+
103
+ *Section* 's are lot like Tables, in the sense that you can pass a collection and have that section repeated for each member of the collection. *But*, Sections can have anything inside it, even Tables, as long as you pass the appropriate data structure.
104
+
105
+ Let's see an example:
106
+
107
+ <pre>
108
+
109
+ @invoices = Invoice.find(:all)
110
+
111
+ report = ODFReport::Report.new("reports/invoice.odt") do |r|
112
+
113
+ r.add_field(:title, "INVOICES REPORT")
114
+ r.add_field(:date, Date.today)
115
+
116
+ r.add_section("SC_INVOICE", @invoices) do |s|
117
+
118
+ s.add_field(:number) { |invoice| invoice.number.to_s.rjust(5, '0') }
119
+ s.add_field(:name, :customer_name)
120
+ s.add_field(:address, :customer_address)
121
+
122
+ s.add_table("TB_ITEMS", :items, :header => true) do |t|
123
+ t.add_column(:id)
124
+ t.add_column(:product) {|item| item.product.name }
125
+ t.add_column(:value, :product_value)
126
+ end
127
+
128
+ s.add_field(:total) do |invoice|
129
+ if invoice.status == 'CLOSED'
130
+ invoice.total
131
+ else
132
+ invoice.items.sum('product_value')}
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+ </pre>
140
+
141
+ Note that when you add a Table to a Section, you don't pass the collection itself, but the attribute of the item of that section that's gonna return the collection for that particular Table. Sounds complicated, huh? But once you get it, it's quite straightforward.
142
+
143
+ In the above example, @s.add_table("TB_ITEMS", :items, :header => true) do |t|@, the @:items@ thing refers to a @invoice.items@. Easy, right?
99
144
 
100
145
  <hr/><br/>
101
146
 
102
- h4. Step 2 -- generating the document
147
+ h3. Step 2 -- generating the document
103
148
 
104
149
  It's fairly simple to generate the document. You can use this inside a Rails application or in a standalone script.
105
150
 
106
- h4. Generating a document in a Rails application
151
+ h4. Generating a document in a Rails application
107
152
 
108
153
  In a controller, you can have a code like this:
109
-
154
+
110
155
  <pre>
111
156
  def print
112
157
 
113
158
  @ticket = Ticket.find(params[:id])
114
159
 
115
- report = ODFReport.new("#{RAILS_ROOT}/app/reports/ticket.odt") do |r|
116
-
117
- r.add_field(:id, @ticket.id.to_s)
160
+ report = ODFReport::Report.new("#{RAILS_ROOT}/app/reports/ticket.odt") do |r|
161
+
162
+ r.add_field(:id, @ticket.id.to_s)
118
163
  r.add_field(:created_by, @ticket.created_by)
119
164
  r.add_field(:created_at, @ticket.created_at.strftime("%d/%m/%Y - %H:%M"))
120
- r.add_field(:type, @ticket.type.name)
121
- r.add_field(:status, @ticket.status_text)
122
- r.add_field(:date, Time.now.strftime("%d/%m/%Y - %H:%M"))
123
- r.add_field(:solution, (@ticket.solution || ''))
165
+ r.add_field(:type, @ticket.type.name)
166
+ r.add_field(:status, @ticket.status_text)
167
+ r.add_field(:date, Time.now.strftime("%d/%m/%Y - %H:%M"))
168
+ r.add_field(:solution, (@ticket.solution || ''))
124
169
 
125
- r.add_table("OPERATORS", @ticket.operators) do | row, op |
126
- row["OPERATOR_NAME"] = "#{op.name} (#{op.department.short_name})"
170
+ r.add_table("OPERATORS", @ticket.operators) do |t|
171
+ t.add_column(:operator_name) { |op| "#{op.name} (#{op.department.short_name})" }
127
172
  end
128
-
129
- r.add_table("FIELDS", @ticket.fields) do | row, field |
130
-
131
- if field.is_a?(String)
132
- row["FIELD_NAME"] = 'Materials'
133
- row["FIELD_VALUE"] = field
134
- else
135
- row["FIELD_NAME"] = field.name
136
- row["FIELD_VALUE"] = field.text_value || ''
137
- end
138
173
 
174
+ r.add_table("FIELDS", @ticket.fields) do |t|
175
+ t.add_column(:field_name, :name)
176
+ t.add_column(:field_value) { |field| field.text_value || "Empty" }
139
177
  end
140
178
 
141
179
  end
142
180
 
143
181
  report_file_name = report.generate
144
182
 
145
- send_file(report_file_name)
183
+ send_file(report_file_name)
146
184
 
147
185
  end
148
186
  </pre>
@@ -151,12 +189,12 @@ The @generate@ method will, er... generate the document in a temp dir and return
151
189
 
152
190
  _That's all I have to say about that._
153
191
 
154
- h4. Generating a document in a standalone script
192
+ h4. Generating a document in a standalone script
155
193
 
156
194
  It's just the same as in a Rails app, but you can inform the path where the file will be generated instead of using a temp dir.
157
195
 
158
196
  <pre>
159
- report = ODFReport.new("ticket.odt") do |r|
197
+ report = ODFReport::Report.new("ticket.odt") do |r|
160
198
 
161
199
  ... populates the report ...
162
200
 
data/lib/odf-report.rb CHANGED
@@ -1,218 +1,8 @@
1
1
  require 'rubygems'
2
2
  require 'zip/zipfilesystem'
3
3
  require 'fileutils'
4
-
5
- class ODFReport
6
-
7
- def initialize(template_name, &block)
8
- @template = template_name
9
- @data={:values=>{}, :tables=>{}, :images => {} }
10
-
11
- @tmp_dir = Dir.tmpdir + "/" + random_filename(:prefix=>'odt_')
12
- Dir.mkdir(@tmp_dir) unless File.exists? @tmp_dir
13
-
14
- yield self
15
- end
16
-
17
- def add_field(field_tag, value)
18
- @data[:values][field_tag] = value
19
- end
20
-
21
- def add_table(table_tag, collection, &block)
22
-
23
- @data[:tables][table_tag] = []
24
-
25
- collection.each do |item|
26
- row = {}
27
- yield(row, item)
28
- @data[:tables][table_tag] << row
29
- end
30
-
31
- end
32
-
33
- def add_image(name, path)
34
- @data[:images][name] = path
35
- end
36
-
37
- def generate(dest = nil)
38
-
39
- if dest
40
-
41
- FileUtils.cp(@template, dest)
42
- new_file = dest
43
-
44
- else
45
-
46
- FileUtils.cp(@template, @tmp_dir)
47
- new_file = "#{@tmp_dir}/#{File.basename(@template)}"
48
-
49
- end
50
-
51
- %w(content.xml styles.xml).each do |content_file|
52
-
53
- update_file_from_zip(new_file, content_file) do |txt|
54
-
55
- replace_fields!(txt)
56
- replace_tables!(txt)
57
- replace_image_refs!(txt)
58
- end
59
-
60
- end
61
-
62
- unless @data[:images].empty?
63
- image_dir_name = "Pictures"
64
- dir = File.join("#{@tmp_dir}", image_dir_name)
65
- add_image_files_to_dir(dir)
66
- add_dir_to_zip(new_file, dir, image_dir_name)
67
- end
68
-
69
- new_file
70
-
71
- end
72
-
73
- private
74
-
75
- def add_image_files_to_dir(dir)
76
- FileUtils.mkdir(dir)
77
- @data[:images].each_pair do |name, path|
78
- FileUtils.cp(path, File.join(dir, File.basename(path)))
79
- end
80
- end
81
-
82
- def add_dir_to_zip(zip_file, dir, entry)
83
- Zip::ZipFile.open(zip_file, true) do |z|
84
- Dir["#{dir}/**/*"].each { |f| z.add("#{entry}/#{File.basename(f)}", f) }
85
- end
86
- end
87
-
88
- def update_file_from_zip(zip_file, content_file, &block)
89
-
90
- Zip::ZipFile.open(zip_file) do |z|
91
- cont = "#{@tmp_dir}/#{content_file}"
92
-
93
- z.extract(content_file, cont)
94
-
95
- txt = ''
96
-
97
- File.open(cont, "r") do |f|
98
- txt = f.read
99
- end
100
-
101
- yield(txt)
102
-
103
- File.open(cont, "w") do |f|
104
- f.write(txt)
105
- end
106
-
107
- z.replace(content_file, cont)
108
- end
109
-
110
- end
111
-
112
-
113
- def replace_fields!(content)
114
- hash_gsub!(content, @data[:values])
115
- end
116
-
117
- def replace_image_refs!(content)
118
- @data[:images].each_pair do |image_name, path|
119
- #Set the new image path
120
- new_path = File.join("Pictures", File.basename(path))
121
- #Search for the image
122
- image_rgx = Regexp.new("draw:name=\"#{image_name}\".*?><draw:image.*?xlink:href=\"([^\s]*)\" .*?/></draw:frame>")
123
- content_match = content.match(image_rgx)
124
- if content_match
125
- replace_path = content_match[1]
126
- content.gsub!(content_match[0], content_match[0].gsub(replace_path, new_path))
127
- end
128
- end
129
- end
130
-
131
- def replace_tables!(content)
132
-
133
- @data[:tables].each do |table_name, records|
134
-
135
- # search for the table inside the content
136
- table_rgx = Regexp.new("(<table:table table:name=\"#{table_name}.*?>.*?<\/table:table>)", "m")
137
- table_match = content.match(table_rgx)
138
-
139
- if table_match
140
- table = table_match[0]
141
-
142
- # extract the table from the content
143
- content.gsub!(table, "[TABLE_#{table_name}]")
144
-
145
- # search for the table:row's
146
- row_rgx = Regexp.new("(<table:table-row.*?<\/table:table-row>)", "m")
147
-
148
- # use scan (instead of match) as the table can have more than one table-row (header and data)
149
- # and scan returns all matches
150
- row_match = table.scan(row_rgx)
151
-
152
- unless row_match.empty?
153
-
154
- # If there more than one line in the table, takes the second entry (row_match[1])
155
- # since the first one represents the column header.
156
- # If there just one line, takes the first line. Besides, since the entry is an Array itself,
157
- # takes the entry's first element ( entry[0] )
158
- model_row = (row_match[1] || row_match[0])[0]
159
-
160
- # extract the row from the table
161
- table.gsub!(model_row, "[ROW_#{table_name}]")
162
-
163
- new_rows = ""
164
-
165
- # for each record
166
- records.each do |_values|
167
-
168
- # generates one new row (table-row), based in the model extracted
169
- # from the original table
170
- tmp_row = model_row.dup
171
-
172
- # replace values in the model_row and stores in new_rows
173
- hash_gsub!(tmp_row, _values)
174
-
175
- new_rows << tmp_row
176
- end
177
-
178
- # replace back the lines into the table
179
- table.gsub!("[ROW_#{table_name}]", new_rows)
180
-
181
- end # unless row_match.empty?
182
-
183
- # replace back the table into content
184
- content.gsub!("[TABLE_#{table_name}]", table)
185
-
186
- end # if table match
187
-
188
- end # tables each
189
-
190
- end # replace_tables
191
-
192
- def hash_gsub!(_text, hash_of_values)
193
- hash_of_values.each do |key, val|
194
- _text.gsub!("[#{key.to_s.upcase}]", html_escape(val)) unless val.nil?
195
- end
196
- end
197
-
198
- def random_filename(opts={})
199
- opts = {:chars => ('0'..'9').to_a + ('A'..'F').to_a + ('a'..'f').to_a,
200
- :length => 24, :prefix => '', :suffix => '',
201
- :verify => true, :attempts => 10}.merge(opts)
202
- opts[:attempts].times do
203
- filename = ''
204
- opts[:length].times { filename << opts[:chars][rand(opts[:chars].size)] }
205
- filename = opts[:prefix] + filename + opts[:suffix]
206
- return filename unless opts[:verify] && File.exists?(filename)
207
- end
208
- nil
209
- end
210
-
211
- HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
212
-
213
- def html_escape(s)
214
- return "" unless s
215
- s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
216
- end
217
-
218
- end
4
+ require 'odf-report/file_ops'
5
+ require 'odf-report/hash_gsub'
6
+ require 'odf-report/section'
7
+ require 'odf-report/table'
8
+ require 'odf-report/report'
@@ -0,0 +1,57 @@
1
+ module ODFReport
2
+
3
+ module FileOps
4
+
5
+ def random_filename(opts={})
6
+ opts = {:chars => ('0'..'9').to_a + ('A'..'F').to_a + ('a'..'f').to_a,
7
+ :length => 24, :prefix => '', :suffix => '',
8
+ :verify => true, :attempts => 10}.merge(opts)
9
+ opts[:attempts].times do
10
+ filename = ''
11
+ opts[:length].times { filename << opts[:chars][rand(opts[:chars].size)] }
12
+ filename = opts[:prefix] + filename + opts[:suffix]
13
+ return filename unless opts[:verify] && File.exists?(filename)
14
+ end
15
+ nil
16
+ end
17
+
18
+ def add_files_to_dir(files, dir)
19
+ FileUtils.mkdir(dir)
20
+ files.each do |path|
21
+ FileUtils.cp(path, File.join(dir, File.basename(path)))
22
+ end
23
+ end
24
+
25
+ def add_dir_to_zip(zip_file, dir, entry)
26
+ Zip::ZipFile.open(zip_file, true) do |z|
27
+ Dir["#{dir}/**/*"].each { |f| z.add("#{entry}/#{File.basename(f)}", f) }
28
+ end
29
+ end
30
+
31
+ def update_file_from_zip(zip_file, content_file, &block)
32
+
33
+ Zip::ZipFile.open(zip_file) do |z|
34
+ cont = "#{@tmp_dir}/#{content_file}"
35
+
36
+ z.extract(content_file, cont)
37
+
38
+ txt = ''
39
+
40
+ File.open(cont, "r") do |f|
41
+ txt = f.read
42
+ end
43
+
44
+ yield(txt)
45
+
46
+ File.open(cont, "w") do |f|
47
+ f.write(txt)
48
+ end
49
+
50
+ z.replace(content_file, cont)
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,20 @@
1
+ module ODFReport
2
+
3
+ module HashGsub
4
+
5
+ def hash_gsub!(_text, hash_of_values)
6
+ hash_of_values.each do |key, val|
7
+ _text.gsub!("[#{key.to_s.upcase}]", html_escape(val))
8
+ end
9
+ end
10
+
11
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
12
+
13
+ def html_escape(s)
14
+ return "" unless s
15
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,127 @@
1
+ module ODFReport
2
+
3
+ class Report
4
+ include HashGsub, FileOps
5
+
6
+ attr_accessor :values, :tables, :images, :sections
7
+
8
+ def initialize(template_name, &block)
9
+ @template = template_name
10
+
11
+ @values = {}
12
+ @tables = []
13
+ @images = {}
14
+ @sections = []
15
+
16
+ @tmp_dir = Dir.tmpdir + "/" + random_filename(:prefix=>'odt_')
17
+ Dir.mkdir(@tmp_dir) unless File.exists? @tmp_dir
18
+
19
+ yield(self)
20
+
21
+ end
22
+
23
+ def add_section(section_name, collection, &block)
24
+ sec = Section.new(section_name)
25
+ @sections << sec
26
+
27
+ yield(sec)
28
+
29
+ sec.populate(collection)
30
+
31
+ end
32
+
33
+ def add_field(field_tag, value)
34
+ @values[field_tag] = value || ''
35
+ end
36
+
37
+ def add_table(table_name, collection, opts={}, &block)
38
+ opts[:name] = table_name
39
+ tab = Table.new(opts)
40
+ yield(tab)
41
+ @tables << tab
42
+
43
+ tab.populate(collection)
44
+
45
+ end
46
+
47
+ def add_image(name, path)
48
+ @images[name] = path
49
+ end
50
+
51
+ def generate(dest = nil)
52
+
53
+ if dest
54
+
55
+ FileUtils.cp(@template, dest)
56
+ new_file = dest
57
+
58
+ else
59
+
60
+ FileUtils.cp(@template, @tmp_dir)
61
+ new_file = "#{@tmp_dir}/#{File.basename(@template)}"
62
+
63
+ end
64
+
65
+ %w(content.xml styles.xml).each do |content_file|
66
+
67
+ update_file_from_zip(new_file, content_file) do |txt|
68
+
69
+ replace_fields!(txt)
70
+ replace_tables!(txt)
71
+ replace_image_refs!(txt)
72
+ replace_sections!(txt)
73
+
74
+ end
75
+
76
+ end
77
+
78
+ unless @images.empty?
79
+ image_dir_name = "Pictures"
80
+ dir = File.join("#{@tmp_dir}", image_dir_name)
81
+ add_files_to_dir(@images.values, dir)
82
+ add_dir_to_zip(new_file, dir, image_dir_name)
83
+ end
84
+
85
+ new_file
86
+
87
+ end
88
+
89
+ private
90
+
91
+ def replace_fields!(content)
92
+ hash_gsub!(content, @values)
93
+ end
94
+
95
+ def replace_tables!(content)
96
+
97
+ @tables.each do |table|
98
+ table.replace!(content)
99
+ end
100
+
101
+ end
102
+
103
+ def replace_sections!(content)
104
+
105
+ @sections.each do |section|
106
+ section.replace!(content)
107
+ end
108
+
109
+ end
110
+
111
+ def replace_image_refs!(content)
112
+ @images.each_pair do |image_name, path|
113
+ #Set the new image path
114
+ new_path = File.join("Pictures", File.basename(path))
115
+ #Search for the image
116
+ image_rgx = Regexp.new("draw:name=\"#{image_name}\".*?><draw:image.*?xlink:href=\"([^\s]*)\" .*?/></draw:frame>")
117
+ content_match = content.match(image_rgx)
118
+ if content_match
119
+ replace_path = content_match[1]
120
+ content.gsub!(content_match[0], content_match[0].gsub(replace_path, new_path))
121
+ end
122
+ end
123
+ end
124
+
125
+ end
126
+
127
+ end
@@ -0,0 +1,126 @@
1
+ module ODFReport
2
+
3
+ class Section
4
+ include HashGsub
5
+
6
+ attr_accessor :fields, :tables, :data, :name
7
+
8
+ def initialize(name)
9
+ @name = name
10
+
11
+ @fields = {}
12
+ @data = []
13
+ @tables = []
14
+ end
15
+
16
+ def add_field(name, field=nil, &block)
17
+ if field
18
+ @fields[name] = lambda { |item| item.send(field)}
19
+ else
20
+ @fields[name] = block
21
+ end
22
+ end
23
+
24
+ def add_table(table_name, collection_field, opts={}, &block)
25
+ opts.merge!(:name => table_name, :collection_field => collection_field)
26
+ tab = Table.new(opts)
27
+ yield(tab)
28
+ @tables << tab
29
+
30
+ end
31
+
32
+ def populate(collection)
33
+
34
+ collection.each do |item|
35
+ row = {}
36
+ @fields.each do |field_name, block1|
37
+ row[field_name] = block1.call(item)
38
+ end
39
+
40
+ row[:tables] = {}
41
+ @tables.each do |table|
42
+ collection = get_collection_from_item(item, table.collection_field)
43
+ row[:tables][table.name] = table.values(collection)
44
+ end
45
+
46
+ @data << row
47
+ end
48
+
49
+ end
50
+
51
+ def replace!(content)
52
+
53
+ # search for the table inside the content
54
+ section_rgx = Regexp.new("(<text:section.*?text:name=\"#{@name}.*?>(.*?)<\/text:section>)", "m")
55
+ section_match = content.match(section_rgx)
56
+
57
+ if section_match
58
+ section_full = section_match[0]
59
+ section_content = section_match[2]
60
+
61
+ # extract the section from the content
62
+ content.gsub!(section_full, "[SECTION_#{@name}]")
63
+
64
+ new_content = ""
65
+
66
+ # for each record
67
+ @data.each do |_values|
68
+
69
+ # generates one new row (table-row), based in the model extracted
70
+ # from the original table
71
+ tmp_row = section_content.dup
72
+
73
+ # replace values in the section_content and stores in new_content
74
+ hash_gsub!(tmp_row, _values)
75
+
76
+ @tables.each do |t|
77
+ t.replace!(tmp_row, _values[:tables][t.name])
78
+ end
79
+
80
+ new_content << tmp_row
81
+ end
82
+
83
+ # replace back the table into content
84
+ content.gsub!("[SECTION_#{@name}]", new_content)
85
+
86
+ end # if table match
87
+
88
+ end # replace_section
89
+
90
+ private
91
+
92
+ def get_collection_from_item(item, collection_field)
93
+
94
+ if collection_field.is_a?(Array)
95
+ tmp = item.dup
96
+ collection_field.each do |f|
97
+ if f.is_a?(Hash)
98
+ tmp = tmp.send(f.keys[0], f.values[0])
99
+ else
100
+ tmp = tmp.send(f)
101
+ end
102
+ end
103
+ collection = tmp
104
+ else
105
+ collection = item.send(collection_field)
106
+ end
107
+
108
+ return collection
109
+ end
110
+
111
+ def hash_gsub!(_text, hash_of_values)
112
+ hash_of_values.each do |key, val|
113
+ _text.gsub!("[#{key.to_s.upcase}]", html_escape(val))
114
+ end
115
+ end
116
+
117
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
118
+
119
+ def html_escape(s)
120
+ return "" unless s
121
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,144 @@
1
+ module ODFReport
2
+
3
+ class Table
4
+ include HashGsub
5
+
6
+ attr_accessor :fields, :rows, :name, :collection_field, :data, :header
7
+
8
+ def initialize(opts)
9
+ @name = opts[:name]
10
+ @collection_field = opts[:collection_field]
11
+ @collection = opts[:collection]
12
+ @header = opts[:header] || false
13
+
14
+ @fields = {}
15
+ @rows = []
16
+ @data = []
17
+ end
18
+
19
+ def add_column(name, field=nil, &block)
20
+ if field
21
+ @fields[name] = lambda { |item| item.send(field)}
22
+ elsif block_given?
23
+ @fields[name] = block
24
+ else
25
+ @fields[name] = lambda { |item| item.send(name)}
26
+ end
27
+ end
28
+
29
+ def values(collection)
30
+ ret = []
31
+ collection.each do |item|
32
+ row = {}
33
+ @fields.each do |field_name, block1|
34
+ row[field_name] = block1.call(item) || ''
35
+ end
36
+ ret << row
37
+ end
38
+ ret
39
+ end
40
+
41
+ def populate(collection)
42
+ @data = values(collection)
43
+ end
44
+
45
+ def replace!(content, rows = nil)
46
+ @data = rows if rows
47
+
48
+ # search for the table inside the content
49
+ table_rgx = Regexp.new("(<table:table table:name=\"#{@name}.*?>.*?<\/table:table>)", "m")
50
+ table_match = content.match(table_rgx)
51
+
52
+ if table_match
53
+ table = table_match[0]
54
+
55
+ # extract the table from the content
56
+ content.gsub!(table, "[TABLE_#{@name}]")
57
+
58
+ # search for the table:row's
59
+ row_rgx = Regexp.new("(<table:table-row.*?<\/table:table-row>)", "m")
60
+
61
+ # use scan (instead of match) as the table can have more than one table-row (header and data)
62
+ # and scan returns all matches
63
+ row_match = table.scan(row_rgx)
64
+
65
+ unless row_match.empty?
66
+
67
+ replace_rows!(table, row_match)
68
+
69
+ new_rows = ""
70
+
71
+ # for each record
72
+ @data.each do |_values|
73
+
74
+ # generates one new row (table-row), based in the model extracted
75
+ # from the original table
76
+ tmp_row = get_next_row.dup
77
+
78
+ # replace values in the model_row and stores in new_rows
79
+ hash_gsub!(tmp_row, _values)
80
+
81
+ new_rows << tmp_row
82
+ end
83
+
84
+ # replace back the lines into the table
85
+ table.gsub!("[ROW_#{@name}]", new_rows)
86
+
87
+ end # unless row_match.empty?
88
+
89
+ # replace back the table into content
90
+ if @data.empty?
91
+ content.gsub!("[TABLE_#{@name}]", "")
92
+ else
93
+ content.gsub!("[TABLE_#{@name}]", table)
94
+ end
95
+
96
+ end # if table match
97
+
98
+ end # replace
99
+
100
+ private
101
+
102
+ def replace_rows!(table, rows)
103
+
104
+ rows.delete_at(0) if @header # ignore the header
105
+
106
+ @rows = rows.dup
107
+ @row_cursor = 0
108
+
109
+ # extract the rows from the table
110
+ first = rows.delete_at(0)[0]
111
+ table.gsub!(first, "[ROW_#{@name}]")
112
+
113
+ rows.each do |r|
114
+ table.gsub!(r[0], "")
115
+ end
116
+
117
+ end
118
+
119
+ def get_next_row
120
+ ret = @rows[@row_cursor]
121
+ if @rows.size == @row_cursor + 1
122
+ @row_cursor = 0
123
+ else
124
+ @row_cursor += 1
125
+ end
126
+ return ret[0]
127
+ end
128
+
129
+ def hash_gsub!(_text, hash_of_values)
130
+ hash_of_values.each do |key, val|
131
+ _text.gsub!("[#{key.to_s.upcase}]", html_escape(val))
132
+ end
133
+ end
134
+
135
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
136
+
137
+ def html_escape(s)
138
+ return "" unless s
139
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
140
+ end
141
+
142
+ end
143
+
144
+ end
data/odf-report.gemspec CHANGED
@@ -2,21 +2,20 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{odf-report}
5
- s.version = "0.1.3"
5
+ s.version = "0.3.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Sandro Duarte"]
9
- s.date = %q{2009-07-28}
9
+ s.date = %q{2010-05-18}
10
10
  s.description = %q{Generates ODF files, given a template (.odt) and data, replacing tags}
11
11
  s.email = %q{sandrods@gmail.com}
12
12
  s.extra_rdoc_files = ["lib/odf-report.rb", "README.textile"]
13
- s.files = ["lib/odf-report.rb", "odf-report.gemspec", "Rakefile", "README.textile", "test/test.odt", "test/test.rb", "Manifest"]
14
- s.has_rdoc = true
13
+ s.files = %w{lib/odf-report.rb odf-report.gemspec README.textile test/test.odt test/test.rb Manifest lib/odf-report/report.rb lib/odf-report/table.rb lib/odf-report/section.rb lib/odf-report/file_ops.rb lib/odf-report/hash_gsub.rb }
14
+ s.has_rdoc = false
15
15
  s.homepage = %q{}
16
16
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Odf-report", "--main", "README.textile"]
17
17
  s.require_paths = ["lib"]
18
- s.rubyforge_project = %q{odf-report}
19
- s.rubygems_version = %q{1.3.1}
18
+ s.rubygems_version = %q{1.3.6}
20
19
  s.summary = %q{Generates ODF files, given a template (.odt) and data, replacing tags}
21
20
 
22
21
  if s.respond_to? :specification_version then
data/test/test.odt CHANGED
Binary file
data/test/test.rb CHANGED
@@ -1,35 +1,36 @@
1
1
  require '../lib/odf-report'
2
+ require 'ostruct'
2
3
 
3
4
  col1 = []
4
- col1 << {:name=>"name 01", :id=>"01", :address=>"this is address 01"}
5
- col1 << {:name=>"name 03", :id=>"03", :address=>"this is address 03"}
6
- col1 << {:name=>"name 02", :id=>"02", :address=>"this is address 02"}
7
- col1 << {:name=>"name 04", :id=>"04", :address=>"this is address 04"}
5
+ (1..15).each do |i|
6
+ col1 << OpenStruct.new({:name=>"name #{i}", :id=>i, :address=>"this is address #{i}"})
7
+ end
8
+
8
9
 
9
10
  col2 = []
10
- col2 << {:name=>"josh harnet", :id=>"02", :address=>"testing <&> ", :phone=>99025668, :zip=>"90420-002"}
11
- col2 << {:name=>"sandro", :id=>"45", :address=>"address with &", :phone=>88774451, :zip=>"90490-002"}
12
- col2 << {:name=>"ellen bicca", :id=>"77", :address=>"<address with escaped html>", :phone=>77025668, :zip=>"94420-002"}
11
+ col2 << OpenStruct.new({:name=>"josh harnet", :id=>"02", :address=>"testing <&> ", :phone=>99025668, :zip=>"90420-002"})
12
+ col2 << OpenStruct.new({:name=>"sandro duarte", :id=>"45", :address=>"address with &", :phone=>88774451, :zip=>"90490-002"})
13
+ col2 << OpenStruct.new({:name=>"ellen bicca", :id=>"77", :address=>"<address with escaped html>", :phone=>77025668, :zip=>"94420-002"})
13
14
 
14
- report = ODFReport.new("test.odt") do |r|
15
+ report = ODFReport::Report.new("test.odt") do |r|
15
16
 
16
- r.add_field("HEADER_FIELD", "This &field was in the HEADER")
17
+ r.add_field("HEADER_FIELD", "This field was in the HEADER")
17
18
 
18
19
  r.add_field("TAG_01", "New tag")
19
20
  r.add_field("TAG_02", "TAG-2 -> New tag")
20
21
 
21
- r.add_table("TABLE_01", col1) do |row, item|
22
- row["FIELD_01"] = item[:id]
23
- row["FIELD_02"] = item[:name]
24
- row["FIELD_03"] = item[:address]
22
+ r.add_table("TABLE_01", col1, :header=>true) do |t|
23
+ t.add_column(:field_01, :id)
24
+ t.add_column(:field_02, :name)
25
+ t.add_column(:field_03, :address)
25
26
  end
26
27
 
27
- r.add_table("TABLE_02", col2) do |row, item|
28
- row["FIELD_04"] = item[:id]
29
- row["FIELD_05"] = item[:name]
30
- row["FIELD_06"] = item[:address]
31
- row["FIELD_07"] = item[:phone]
32
- row["FIELD_08"] = item[:zip]
28
+ r.add_table("TABLE_02", col2) do |t|
29
+ t.add_column(:field_04, :id)
30
+ t.add_column(:field_05, :name)
31
+ t.add_column(:field_06, :address)
32
+ t.add_column(:field_07, :phone)
33
+ t.add_column(:field_08, :zip)
33
34
  end
34
35
 
35
36
  r.add_image("graphics1", File.join(Dir.pwd, 'piriapolis.jpg'))
@@ -37,3 +38,40 @@ report = ODFReport.new("test.odt") do |r|
37
38
  end
38
39
 
39
40
  report.generate("result.odt")
41
+
42
+ class Item
43
+ attr_accessor :name, :sid, :children
44
+ def initialize(_name, _sid, _children=[])
45
+ @name=_name
46
+ @sid=_sid
47
+ @children=_children
48
+ end
49
+ end
50
+
51
+ items = []
52
+ items << Item.new("Dexter Morgan", '007', %w(sawyer juliet hurley locke jack freckles))
53
+ items << Item.new("Danny Crane", '302', %w(sidney sloane jack michael marshal))
54
+ items << Item.new("Coach Taylor", '220', %w(meredith christina izzie alex george))
55
+
56
+ report = ODFReport::Report.new("sections.odt") do |r|
57
+
58
+ r.add_field("TAG_01", "New tag")
59
+ r.add_field("TAG_02", "TAG-2 -> New tag")
60
+
61
+ r.add_section("SECTION_01", items) do |s|
62
+
63
+ s.add_field('NAME') do |i|
64
+ i.name
65
+ end
66
+
67
+ s.add_field('SID', :sid)
68
+
69
+ s.add_table('TABLE_S1', :children, :header=>true) do |t|
70
+ t.add_column('NAME1') { |item| "-> #{item}" }
71
+ t.add_column('INV') { |item| item.to_s.reverse.upcase }
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ report.generate("section_result.odt")
metadata CHANGED
@@ -1,7 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: odf-report
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
5
10
  platform: ruby
6
11
  authors:
7
12
  - Sandro Duarte
@@ -9,22 +14,28 @@ autorequire:
9
14
  bindir: bin
10
15
  cert_chain: []
11
16
 
12
- date: 2009-07-28 00:00:00 -03:00
17
+ date: 2010-05-18 00:00:00 -03:00
13
18
  default_executable:
14
19
  dependencies:
15
20
  - !ruby/object:Gem::Dependency
16
21
  name: rubyzip
17
- type: :runtime
18
- version_requirement:
19
- version_requirements: !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
20
24
  requirements:
21
25
  - - ">="
22
26
  - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
23
29
  version: "0"
24
30
  - - "="
25
31
  - !ruby/object:Gem::Version
32
+ segments:
33
+ - 0
34
+ - 9
35
+ - 1
26
36
  version: 0.9.1
27
- version:
37
+ type: :runtime
38
+ version_requirements: *id001
28
39
  description: Generates ODF files, given a template (.odt) and data, replacing tags
29
40
  email: sandrods@gmail.com
30
41
  executables: []
@@ -37,11 +48,15 @@ extra_rdoc_files:
37
48
  files:
38
49
  - lib/odf-report.rb
39
50
  - odf-report.gemspec
40
- - Rakefile
41
51
  - README.textile
42
52
  - test/test.odt
43
53
  - test/test.rb
44
54
  - Manifest
55
+ - lib/odf-report/report.rb
56
+ - lib/odf-report/table.rb
57
+ - lib/odf-report/section.rb
58
+ - lib/odf-report/file_ops.rb
59
+ - lib/odf-report/hash_gsub.rb
45
60
  has_rdoc: true
46
61
  homepage: ""
47
62
  licenses: []
@@ -60,18 +75,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
60
75
  requirements:
61
76
  - - ">="
62
77
  - !ruby/object:Gem::Version
78
+ segments:
79
+ - 0
63
80
  version: "0"
64
- version:
65
81
  required_rubygems_version: !ruby/object:Gem::Requirement
66
82
  requirements:
67
83
  - - ">="
68
84
  - !ruby/object:Gem::Version
85
+ segments:
86
+ - 1
87
+ - 2
69
88
  version: "1.2"
70
- version:
71
89
  requirements: []
72
90
 
73
- rubyforge_project: odf-report
74
- rubygems_version: 1.3.5
91
+ rubyforge_project:
92
+ rubygems_version: 1.3.6
75
93
  signing_key:
76
94
  specification_version: 2
77
95
  summary: Generates ODF files, given a template (.odt) and data, replacing tags
data/Rakefile DELETED
@@ -1,14 +0,0 @@
1
- require 'rubygems'
2
- require 'rake'
3
- require 'echoe'
4
-
5
- Echoe.new('odf-report', '0.1.1') do |p|
6
- p.description = "Generates ODF files, given a template (.odt) and data, replacing tags"
7
- p.url = ""
8
- p.author = "Sandro Duarte"
9
- p.email = "sandrods@gmail.com"
10
- p.ignore_pattern = ["tmp/*", "script/*"]
11
- p.development_dependencies = []
12
- p.runtime_dependencies = ['rubyzip >= 0.9.1']
13
- end
14
-