odf-report 0.5.1 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +5 -13
  2. data/.github/workflows/gem-push.yml +40 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +4 -0
  5. data/CHANGELOG.md +56 -0
  6. data/README.md +220 -0
  7. data/Rakefile +8 -0
  8. data/bin/odt-extract +10 -0
  9. data/bin/odt-viewer +18 -0
  10. data/lib/odf-report.rb +11 -9
  11. data/lib/odf-report/data_source.rb +65 -0
  12. data/lib/odf-report/field.rb +35 -36
  13. data/lib/odf-report/image.rb +57 -0
  14. data/lib/odf-report/nestable.rb +65 -0
  15. data/lib/odf-report/parser/default.rb +5 -4
  16. data/lib/odf-report/report.rb +29 -57
  17. data/lib/odf-report/section.rb +17 -80
  18. data/lib/odf-report/table.rb +52 -81
  19. data/lib/odf-report/template.rb +88 -0
  20. data/lib/odf-report/text.rb +2 -4
  21. data/lib/odf-report/version.rb +1 -1
  22. data/odf-report.gemspec +7 -4
  23. data/spec/fields_spec.rb +77 -0
  24. data/spec/images/image_1.jpg +0 -0
  25. data/spec/images/image_2.jpg +0 -0
  26. data/spec/images/image_3.jpg +0 -0
  27. data/{test → spec/images}/piriapolis.jpg +0 -0
  28. data/spec/images/placeholder.jpg +0 -0
  29. data/{test → spec/images}/rails.png +0 -0
  30. data/spec/images_spec.rb +159 -0
  31. data/spec/sections_spec.rb +51 -0
  32. data/spec/spec_helper.rb +43 -0
  33. data/spec/tables_spec.rb +39 -0
  34. data/spec/template_spec.rb +45 -0
  35. data/spec/templates/images.odt +0 -0
  36. data/spec/templates/specs.odt +0 -0
  37. data/test/fields_inside_text_test.rb +38 -0
  38. data/test/images_test.rb +32 -0
  39. data/test/nested_tables_test.rb +43 -0
  40. data/test/sections_test.rb +44 -0
  41. data/test/sub_sections_test.rb +58 -0
  42. data/test/table_headers_test.rb +41 -0
  43. data/test/tables_test.rb +67 -0
  44. data/test/templates/images/image_1.jpg +0 -0
  45. data/test/templates/images/image_2.jpg +0 -0
  46. data/test/templates/images/image_3.jpg +0 -0
  47. data/test/templates/images/placeholder.jpg +0 -0
  48. data/test/templates/images/placeholder.png +0 -0
  49. data/test/templates/piriapolis.jpg +0 -0
  50. data/test/templates/rails.png +0 -0
  51. data/test/templates/test_images.odt +0 -0
  52. data/test/templates/test_sub_sections.odt +0 -0
  53. data/test/templates/test_text.odt +0 -0
  54. data/test/test.rb +262 -0
  55. data/test/text_test.rb +56 -0
  56. metadata +151 -46
  57. data/README.textile +0 -225
  58. data/lib/odf-report/fields.rb +0 -40
  59. data/lib/odf-report/file.rb +0 -50
  60. data/lib/odf-report/images.rb +0 -44
  61. data/lib/odf-report/nested.rb +0 -34
  62. data/test/test_fields_inside_text.rb +0 -37
  63. data/test/test_nested_tables.rb +0 -39
  64. data/test/test_sections.rb +0 -39
  65. data/test/test_sub_sections.rb +0 -57
  66. data/test/test_table_headers.rb +0 -39
  67. data/test/test_tables.rb +0 -62
  68. data/test/test_text.rb +0 -48
@@ -1,225 +0,0 @@
1
- h1. ODF-REPORT
2
-
3
- Gem for generating .odt files by making strings, images, tables and sections replacements in a previously created .odt file.
4
-
5
- h3. NEW
6
-
7
- * as per popular request, now uses rubyzip ~> 1.1.0
8
- * uses @Zip::OutputStream.write_buffer@ to generate the file. This should avoid corruption issues.
9
- * the @.generate@ method now returns the actual report binary, so you can send_data it directly
10
- * you can also use @.generate('file.odt')@, which saves the report to the specified file
11
-
12
- h2. INSTALL
13
-
14
- In your Gemfile
15
- <pre>gem 'odf-report' </pre>
16
-
17
- h2. USAGE
18
-
19
- h3. Step 1 -- the template
20
-
21
- First of all, you need to create a .odt file to serve as a template
22
-
23
- Templates are normal .odt files with placeholders for Substitutions
24
-
25
- There are now *four* kinds of substitutions available: *fields*, *tables*, *images* and *sections*.
26
-
27
- h3. Fields placeholders
28
-
29
- It's just an upcase sentence, surrounded by brackets. It will be replaced for wathever value you supply.
30
-
31
- In the folowing example:
32
-
33
- <pre>
34
- report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
35
-
36
- r.add_field :user_name, @user.name
37
- r.add_field :address, "My new address"
38
-
39
- end
40
- </pre>
41
-
42
- All occurences of @[USER_NAME]@ found in the file will be replaced by the value of @@user.name@ whereas all @[ADDRESS]@ 'es will contains @My new address@
43
-
44
- It's as simple as that.
45
-
46
-
47
- h3. Table placeholders
48
-
49
- 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.
50
-
51
- 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.
52
-
53
- As with Field placeholders, just insert a @[FIELD_NAME]@ in each cell and let the magic takes place.
54
-
55
- Taking the folowing example:
56
-
57
- <pre>
58
- report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
59
-
60
- r.add_field "USER_NAME", @user.nome
61
- r.add_field "ADDRESS", @user.address
62
-
63
- r.add_table("TABLE_1", @list_of_itens, :header=>true) do |t|
64
- t.add_column(:item_id, :id)
65
- t.add_column(:description) do { |item| "==> #{item.description}" }
66
- end
67
-
68
- end
69
- </pre>
70
-
71
- and considering you have a table like this in your template
72
-
73
- <pre>
74
- ---------------------------------
75
- | [ITEM_ID] | [DESCRIPTION] |
76
- ---------------------------------
77
-
78
- * this is my lame attempt to draw a table.
79
- you don't suppose to type this.
80
- you have to use an actual table.
81
- i don't know... just thought I'd mention it ;-)
82
- </pre>
83
-
84
- and a collection @list_of_itens, it will be created one row for each item in the collection, and the replacement will take place accordingly.
85
-
86
- Any format applied to the fields in the template will be preserved.
87
-
88
-
89
- h3. Images
90
-
91
- 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.
92
- You can also assign any properties you want to the mock image and they will be kept once the image is replaced.
93
-
94
- An image replace would look like this:
95
-
96
- <pre>
97
- report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
98
-
99
- r.add_image :graphics1, "/path/to/the/image.jpg"
100
-
101
- end
102
- </pre>
103
-
104
-
105
- h3. Sections
106
-
107
- 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 Section in OpenOffice is as easy as select menu *Insert* and then *Section...*, and then choose a name for it.
108
-
109
- *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 *and nested Sections*, as long as you pass the appropriate data structure.
110
-
111
- Let's see an example:
112
-
113
- <pre>
114
-
115
- @invoices = Invoice.find(:all)
116
-
117
- report = ODFReport::Report.new("reports/invoice.odt") do |r|
118
-
119
- r.add_field(:title, "INVOICES REPORT")
120
- r.add_field(:date, Date.today)
121
-
122
- r.add_section("SC_INVOICE", @invoices) do |s|
123
-
124
- s.add_field(:number) { |invoice| invoice.number.to_s.rjust(5, '0') }
125
- s.add_field(:name, :customer_name)
126
- s.add_field(:address, :customer_address)
127
-
128
- s.add_table("TB_ITEMS", :items, :header => true) do |t|
129
- t.add_column(:id)
130
- t.add_column(:product) {|item| item.product.name }
131
- t.add_column(:value, :product_value)
132
- end
133
-
134
- s.add_field(:total) do |invoice|
135
- if invoice.status == 'CLOSED'
136
- invoice.total
137
- else
138
- invoice.items.sum('product_value')}
139
- end
140
- end
141
-
142
- s.add_section("SUB_NOTES", :notes) do |s1|
143
-
144
- s1.add_field(:note_title) { |n| n.title }
145
-
146
- s1.add_table ...
147
-
148
- end
149
-
150
- end
151
-
152
- end
153
- </pre>
154
-
155
- 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.
156
-
157
- In the above example, @s.add_table("TB_ITEMS", :items, :header => true) do |t|@, the @:items@ thing refers to a @invoice.items@. Easy, right?
158
-
159
- <hr/><br/>
160
-
161
- h3. Step 2 -- generating the document
162
-
163
- It's fairly simple to generate the document. You can use this inside a Rails application or in a standalone script.
164
-
165
- h4. Generating a document in a Rails application
166
-
167
- In a controller, you can have a code like this:
168
-
169
- <pre>
170
- def print
171
-
172
- @ticket = Ticket.find(params[:id])
173
-
174
- # For Rails 3 or latest replace #{RAILS_ROOT} to #{Rails.root}
175
- report = ODFReport::Report.new("#{RAILS_ROOT}/app/reports/ticket.odt") do |r|
176
-
177
- r.add_field(:id, @ticket.id.to_s)
178
- r.add_field(:created_by, @ticket.created_by)
179
- r.add_field(:created_at, @ticket.created_at.strftime("%d/%m/%Y - %H:%M"))
180
- r.add_field(:type, @ticket.type.name)
181
- r.add_field(:status, @ticket.status_text)
182
- r.add_field(:date, Time.now.strftime("%d/%m/%Y - %H:%M"))
183
- r.add_field(:solution, (@ticket.solution || ''))
184
-
185
- r.add_table("OPERATORS", @ticket.operators) do |t|
186
- t.add_column(:operator_name) { |op| "#{op.name} (#{op.department.short_name})" }
187
- end
188
-
189
- r.add_table("FIELDS", @ticket.fields) do |t|
190
- t.add_column(:field_name, :name)
191
- t.add_column(:field_value) { |field| field.text_value || "Empty" }
192
- end
193
-
194
- end
195
-
196
- send_data report.generate, type: 'application/vnd.oasis.opendocument.text',
197
- disposition: 'attachment',
198
- filename: 'report.odt'
199
-
200
- end
201
- </pre>
202
-
203
-
204
- _That's all I have to say about that._
205
-
206
- h4. Generating a document in a standalone script
207
-
208
- It's just the same as in a Rails app, but you can inform the path where the file will be saved.
209
-
210
- <pre>
211
- report = ODFReport::Report.new("ticket.odt") do |r|
212
-
213
- ... populates the report ...
214
-
215
- end
216
-
217
- report.generate("./documents/new_ticket.odt")
218
- </pre>
219
-
220
- <hr/>
221
-
222
- h3. REQUIREMENTS
223
-
224
- * rubyzip*: for manipulating the contents of the odt file, since it's actually a zip file.
225
- * nokogiri*: for parsing and manipulating the document xml files.
@@ -1,40 +0,0 @@
1
- module ODFReport
2
-
3
- module Fields
4
-
5
- def field_replace!(_node, data_item = nil)
6
-
7
- txt = _node.inner_html
8
-
9
- @fields.each do |f|
10
- val = f.get_value(data_item)
11
- txt.gsub!(f.to_placeholder, sanitize(val))
12
- end
13
-
14
- _node.inner_html = txt
15
-
16
- end
17
-
18
- private
19
-
20
- def sanitize(txt)
21
- txt = html_escape(txt)
22
- txt = odf_linebreak(txt)
23
- txt
24
- end
25
-
26
- HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
27
-
28
- def html_escape(s)
29
- return "" unless s
30
- s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
31
- end
32
-
33
- def odf_linebreak(s)
34
- return "" unless s
35
- s.to_s.gsub("\n", "<text:line-break/>")
36
- end
37
-
38
- end
39
-
40
- end
@@ -1,50 +0,0 @@
1
- module ODFReport
2
- class File
3
-
4
- attr_accessor :data, :output_stream
5
-
6
- def initialize(template)
7
- raise "Template [#{template}] not found." unless ::File.exists? template
8
- @template = template
9
- end
10
-
11
- def update_content
12
- @buffer = Zip::OutputStream.write_buffer do |out|
13
- @output_stream = out
14
- yield self
15
- end
16
- end
17
-
18
- def update_files(*content_files, &block)
19
-
20
- Zip::File.open(@template) do |file|
21
-
22
- file.each do |entry|
23
-
24
- next if entry.directory?
25
-
26
- entry.get_input_stream do |is|
27
-
28
- data = is.sysread
29
-
30
- if content_files.include?(entry.name)
31
- yield data
32
- end
33
-
34
- @output_stream.put_next_entry(entry.name)
35
- @output_stream.write data
36
-
37
- end
38
-
39
- end
40
-
41
- end
42
-
43
- end
44
-
45
- def data
46
- @buffer.string
47
- end
48
-
49
- end
50
- end
@@ -1,44 +0,0 @@
1
- module ODFReport
2
-
3
- module Images
4
-
5
- IMAGE_DIR_NAME = "Pictures"
6
-
7
- def find_image_name_matches(content)
8
-
9
- @images.each_pair do |image_name, path|
10
- if node = content.xpath("//draw:frame[@draw:name='#{image_name}']/draw:image").first
11
- placeholder_path = node.attribute('href').value
12
- @image_names_replacements[path] = ::File.join(IMAGE_DIR_NAME, ::File.basename(placeholder_path))
13
- end
14
- end
15
-
16
- end
17
-
18
- def replace_images(file)
19
-
20
- return if @images.empty?
21
-
22
- @image_names_replacements.each_pair do |path, template_image|
23
-
24
- file.output_stream.put_next_entry(template_image)
25
- file.output_stream.write ::File.read(path)
26
-
27
- end
28
-
29
- end # replace_images
30
-
31
- # newer versions of LibreOffice can't open files with duplicates image names
32
- def avoid_duplicate_image_names(content)
33
-
34
- nodes = content.xpath("//draw:frame[@draw:name]")
35
-
36
- nodes.each_with_index do |node, i|
37
- node.attribute('name').value = "pic_#{i}"
38
- end
39
-
40
- end
41
-
42
- end
43
-
44
- end
@@ -1,34 +0,0 @@
1
- module ODFReport
2
-
3
- module Nested
4
-
5
- def replace_fields!(new_section, data_item)
6
- field_replace!(new_section, data_item)
7
- end
8
-
9
- def get_collection_from_item(item, collection_field)
10
-
11
- return item[collection_field] if item.is_a?(Hash)
12
-
13
- if collection_field.is_a?(Array)
14
- tmp = item.dup
15
- collection_field.each do |f|
16
- if f.is_a?(Hash)
17
- tmp = tmp.send(f.keys[0], f.values[0])
18
- else
19
- tmp = tmp.send(f)
20
- end
21
- end
22
- collection = tmp
23
- elsif collection_field.is_a?(Hash)
24
- collection = item.send(collection_field.keys[0], collection_field.values[0])
25
- else
26
- collection = item.send(collection_field)
27
- end
28
-
29
- return collection
30
- end
31
-
32
- end
33
-
34
- end
@@ -1,37 +0,0 @@
1
- # coding: UTF-8
2
- require '../lib/odf-report'
3
- require 'ostruct'
4
- require 'faker'
5
-
6
- html = <<-HTML
7
- <p>Uniquely promote installed base total linkage via emerging deliverables com [EVENTO_TEXTO_CARTA], unleash cross-media collaboration <strong>[FUNCAO]</strong> [EVENTO_NOME].</p>
8
-
9
- <p>Progressively fashion diverse portals nº <strong>[NUMERO_SECAO]</strong> do local <strong>[NOME_LOCAL]</strong>, situado na <strong>[ENDERECO_LOCAL]</strong> methodologies </p>
10
-
11
- <p>Assertively orchestrate market positioning bandwidth rather than fully researched total linkage. Interactively architect granular e-markets via clicks-and-mortar ROI. Uniquely aggregate compelling.</p>
12
-
13
- <p>Compellingly facilitate functionalized interfaces before wireless models. Compellingly morph parallel systems whereas combinado com o artigo 63, § 2º da Lei nº 9.504/97, abaixo mencionados:</p>
14
-
15
- <p style="margin-left:1.76cm;"><em>Art. 120 - § 1º Compellingly envisioneer high standards in niches without best-of-breed leadership. Phosfluorescently unleash go forward methodologies after bricks-and-clicks niches. Authoritatively. </em></p>
16
-
17
- <p style="margin-left:1.76cm;"><em>Art. 63 - [...] § 2º Enthusiastically parallel task user friendly functionalities whereas exceptional leadership. </em></p>
18
-
19
- <p>Credibly enable multifunctional strategic theme areas and premium communities.</p>
20
-
21
- HTML
22
-
23
-
24
- report = ODFReport::Report.new("./templates/test_fields_inside_text.odt") do |r|
25
-
26
- r.add_text(:body, html)
27
-
28
- r.add_field('EVENTO_TEXTO_CARTA', Faker::Lorem.sentence)
29
- r.add_field('FUNCAO', Faker::Lorem.word)
30
- r.add_field('EVENTO_NOME', Faker::Company.name)
31
-
32
- r.add_field('NUMERO_SECAO', Faker::Number.number(3))
33
- r.add_field('NOME_LOCAL', Faker::Company.name)
34
- r.add_field('ENDERECO_LOCAL', Faker::Address.street_address)
35
- end
36
-
37
- report.generate("./result/test_fields_inside_text.odt")
@@ -1,39 +0,0 @@
1
- require '../lib/odf-report'
2
- require 'ostruct'
3
-
4
- class Item
5
- attr_accessor :name, :sid, :children
6
- def initialize(_name, _sid, _children=[])
7
- @name=_name
8
- @sid=_sid
9
- @children=_children
10
- end
11
- end
12
-
13
- items = []
14
- items << Item.new("LOST", '007', %w(sawyer juliet hurley locke jack freckles))
15
- items << Item.new("ALIAS", '302', %w(sidney sloane jack michael marshal))
16
- items << Item.new("GREY'S ANATOMY", '220', %w(meredith christina izzie alex george))
17
- items << Item.new("BREAKING BAD", '556', %w(pollos gus mike heisenberg))
18
-
19
- report = ODFReport::Report.new("./templates/test_nested_tables.odt") do |r|
20
-
21
- r.add_field("TAG_01", Time.now)
22
- r.add_field("TAG_02", "TAG-2 -> New tag")
23
-
24
- r.add_table("TABLE_MAIN", items) do |s|
25
-
26
- s.add_column('NAME') { |i| i.name }
27
-
28
- s.add_column('SID', :sid)
29
-
30
- s.add_table('TABLE_S1', :children, :header=>true) do |t|
31
- t.add_column('NAME1') { |item| "-> #{item}" }
32
- t.add_column('INV') { |item| item.to_s.reverse.upcase }
33
- end
34
-
35
- end
36
-
37
- end
38
-
39
- report.generate("./result/test_nested_tables.odt")