Skip to content
This repository
Browse code

Added suport for sections

  • Loading branch information...
commit 8f876275c72479d25a7633851524c73ec2b7d88f 1 parent 8c40e63
Sandro Duarte authored May 19, 2010
7  Manifest
... ...
@@ -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
123  README.textile
Source Rendered
... ...
@@ -1,24 +1,24 @@
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 5
 <hr/>
6 6
 
7  
-h3. INSTALL
  7
+h2. INSTALL
8 8
 
9  
-@gem install sandrods-odf-report --source=http://gems.github.com@
  9
+(sudo) gem install odf-report
10 10
 
11 11
 <hr/>
12 12
 
13  
-h3. USAGE
  13
+h2. USAGE
14 14
 
15  
-h4. Step 1  --  the template
  15
+h3. Step 1  --  the template
16 16
 
17 17
 First of all, you need to create a .odt file to serve as a template
18 18
 
19 19
 Templates are normal .odt files with placeholders for Substitutions
20 20
 
21  
-There are now three kinds of substitutions available: *fields*, *tables* and *images*.
  21
+There are now *four* kinds of substitutions available: *fields*, *tables*, *images* and *sections*.
22 22
 
23 23
 h4. Fields placeholders
24 24
 
@@ -27,11 +27,11 @@ It's just an upcase sentence, surrounded by brackets. It will be replaced for wa
27 27
 In the folowing example:
28 28
 
29 29
 <pre>
30  
-report = ODFReport.new("Users/john/my_template.odt") do |r|
  30
+report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
31 31
 
32 32
   r.add_field :user_name, @user.name
33 33
   r.add_field :address, "My new address"
34  
-  
  34
+
35 35
 end
36 36
 </pre>
37 37
 
@@ -43,21 +43,21 @@ h4. Table placeholders
43 43
 
44 44
 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 45
 
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.
  46
+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 47
 
48 48
 As with Field placeholders, just insert a @[FIELD_NAME]@ in each cell and let the magic takes place.
49 49
 
50 50
 Taking the folowing example:
51 51
 
52 52
 <pre>
53  
-report = ODFReport.new("Users/john/my_template.odt") do |r|
  53
+report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
54 54
 
55 55
   r.add_field "USER_NAME", @user.nome
56 56
   r.add_field "ADDRESS", @user.address
57 57
 
58  
-  r.add_table("TABLE_1", @list_of_itens) do |row, item|
59  
-    row["ITEM_ID"] = item.id
60  
-    row["DESCRIPTION"] = "==> #{item.description}"
  58
+  r.add_table("TABLE_1", @list_of_itens, :header=>true) do |t|
  59
+    t.add_column(:item_id, :id)
  60
+    t.add_column(:description) do { |item| "==> #{item.description}" }
61 61
   end
62 62
 
63 63
 end
@@ -70,9 +70,9 @@ and considering you have a table like this in your template
70 70
 |  [ITEM_ID]  |  [DESCRIPTION]  |
71 71
 ---------------------------------
72 72
 
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. 
  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.
76 76
   i don't know... just thought I'd mention it ;-)
77 77
 </pre>
78 78
 
@@ -88,61 +88,98 @@ 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
+h4. Sections (NEW!)
  99
+
  100
+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.
  101
+
  102
+*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.
  103
+
  104
+Let's see an example:
  105
+
  106
+<pre>
  107
+
  108
+  @invoices = Invoice.find(:all)
  109
+
  110
+  report = ODFReport::Report.new("reports/invoice.odt") do |r|
  111
+
  112
+    r.add_field(:title, "INVOICES REPORT")
  113
+    r.add_field(:date, Date.today)
  114
+
  115
+    r.add_section("SC_INVOICE", @invoices) do |s|
  116
+
  117
+      s.add_field(:number) { |invoice| invoice.number.to_s.rjust(5, '0') }
  118
+      s.add_field(:name,    :customer_name)
  119
+      s.add_field(:address, :customer_address)
  120
+
  121
+      s.add_table("TB_ITEMS", :items, :header => true) do |t|
  122
+        t.add_column(:id)
  123
+        t.add_column(:product) {|item| item.product.name }
  124
+        t.add_column(:value, :product_value)
  125
+      end
  126
+
  127
+      s.add_field(:total) do |invoice|
  128
+        if invoice.status == 'CLOSED'
  129
+          invoice.total
  130
+        else
  131
+          invoice.items.sum('product_value')}
  132
+        end
  133
+      end
  134
+
  135
+    end
  136
+
  137
+  end
  138
+</pre>
  139
+
  140
+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.
  141
+
  142
+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 143
 
100 144
 <hr/><br/>
101 145
 
102  
-h4. Step 2  --  generating the document 
  146
+h3. Step 2  --  generating the document
103 147
 
104 148
 It's fairly simple to generate the document. You can use this inside a Rails application or in a standalone script.
105 149
 
106  
-h4. Generating a document in a Rails application 
  150
+h4. Generating a document in a Rails application
107 151
 
108 152
 In a controller, you can have a code like this:
109  
-	
  153
+
110 154
 <pre>
111 155
 def print
112 156
 
113 157
   @ticket = Ticket.find(params[:id])
114 158
 
115  
-  report = ODFReport.new("#{RAILS_ROOT}/app/reports/ticket.odt") do |r|
116  
-   
117  
-    r.add_field(:id, @ticket.id.to_s)
  159
+  report = ODFReport::Report.new("#{RAILS_ROOT}/app/reports/ticket.odt") do |r|
  160
+
  161
+    r.add_field(:id,         @ticket.id.to_s)
118 162
     r.add_field(:created_by, @ticket.created_by)
119 163
     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 || ''))
  164
+    r.add_field(:type,       @ticket.type.name)
  165
+    r.add_field(:status,     @ticket.status_text)
  166
+    r.add_field(:date,       Time.now.strftime("%d/%m/%Y - %H:%M"))
  167
+    r.add_field(:solution,   (@ticket.solution || ''))
124 168
 
125  
-    r.add_table("OPERATORS", @ticket.operators) do | row, op |
126  
-      row["OPERATOR_NAME"] = "#{op.name} (#{op.department.short_name})"
  169
+    r.add_table("OPERATORS", @ticket.operators) do |t|
  170
+      t.add_column(:operator_name) { |op| "#{op.name} (#{op.department.short_name})" }
127 171
     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 172
 
  173
+    r.add_table("FIELDS", @ticket.fields) do |t|
  174
+      t.add_column(:field_name, :name)
  175
+      t.add_column(:field_value) { |field| field.text_value || "Empty" }
139 176
     end
140 177
 
141 178
   end
142 179
 
143 180
   report_file_name = report.generate
144 181
 
145  
-  send_file(report_file_name) 
  182
+  send_file(report_file_name)
146 183
 
147 184
 end
148 185
 </pre>
@@ -151,12 +188,12 @@ The @generate@ method will, er... generate the document in a temp dir and return
151 188
 
152 189
 _That's all I have to say about that._
153 190
 
154  
-h4. Generating a document in a standalone script 
  191
+h4. Generating a document in a standalone script
155 192
 
156 193
 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 194
 
158 195
 <pre>
159  
-report = ODFReport.new("ticket.odt") do |r|
  196
+report = ODFReport::Report.new("ticket.odt") do |r|
160 197
 
161 198
 ... populates the report ...
162 199
 
14  Rakefile
... ...
@@ -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  
-
220  lib/odf-report.rb
... ...
@@ -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'
57  lib/odf-report/file_ops.rb
... ...
@@ -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
20  lib/odf-report/hash_gsub.rb
... ...
@@ -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
127  lib/odf-report/report.rb
... ...
@@ -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
126  lib/odf-report/section.rb
... ...
@@ -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
144  lib/odf-report/table.rb
... ...
@@ -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
BIN  test/sections.odt
Binary file not shown
BIN  test/test.odt
Binary file not shown
76  test/test.rb
... ...
@@ -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 @@
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")

0 notes on commit 8f87627

Please sign in to comment.
Something went wrong with that request. Please try again.