odf-report 0.5.2 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +40 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +62 -0
  5. data/README.md +220 -0
  6. data/bin/odt-extract +10 -0
  7. data/bin/odt-viewer +18 -0
  8. data/lib/odf-report.rb +11 -8
  9. data/lib/odf-report/data_source.rb +65 -0
  10. data/lib/odf-report/field.rb +7 -40
  11. data/lib/odf-report/image.rb +57 -0
  12. data/lib/odf-report/nestable.rb +65 -0
  13. data/lib/odf-report/report.rb +24 -28
  14. data/lib/odf-report/section.rb +15 -42
  15. data/lib/odf-report/table.rb +54 -59
  16. data/lib/odf-report/template.rb +88 -0
  17. data/lib/odf-report/text.rb +1 -3
  18. data/lib/odf-report/version.rb +1 -1
  19. data/odf-report.gemspec +4 -4
  20. data/spec/fields_spec.rb +4 -4
  21. data/spec/images/image_1.jpg +0 -0
  22. data/spec/images/image_2.jpg +0 -0
  23. data/spec/images/image_3.jpg +0 -0
  24. data/spec/images/piriapolis.jpg +0 -0
  25. data/spec/images/placeholder.jpg +0 -0
  26. data/spec/images/rails.png +0 -0
  27. data/spec/images_spec.rb +159 -0
  28. data/spec/sections_spec.rb +51 -0
  29. data/spec/spec_helper.rb +2 -4
  30. data/spec/tables_spec.rb +1 -1
  31. data/spec/template_spec.rb +45 -0
  32. data/spec/templates/images.odt +0 -0
  33. data/spec/templates/specs.odt +0 -0
  34. data/test/images_test.rb +32 -0
  35. data/test/templates/images/image_1.jpg +0 -0
  36. data/test/templates/images/image_2.jpg +0 -0
  37. data/test/templates/images/image_3.jpg +0 -0
  38. data/test/templates/images/placeholder.jpg +0 -0
  39. data/test/templates/images/placeholder.png +0 -0
  40. data/test/templates/test_images.odt +0 -0
  41. data/test/test.rb +262 -0
  42. metadata +77 -23
  43. data/README.textile +0 -223
  44. data/lib/odf-report/file.rb +0 -50
  45. data/lib/odf-report/images.rb +0 -44
  46. data/lib/odf-report/nested.rb +0 -62
  47. data/spec/specs.odt +0 -0
@@ -5,56 +5,25 @@ module ODFReport
5
5
 
6
6
  def initialize(opts, &block)
7
7
  @name = opts[:name]
8
- @data_field = opts[:data_field]
9
-
10
- unless @value = opts[:value]
11
-
12
- if block_given?
13
- @block = block
14
-
15
- else
16
- @block = lambda { |item| self.extract_value(item) }
17
- end
18
-
19
- end
8
+ @data_source = DataSource.new(opts, &block)
9
+ end
20
10
 
11
+ def set_source(record)
12
+ @data_source.set_source(record)
13
+ self
21
14
  end
22
15
 
23
16
  def replace!(content, data_item = nil)
24
17
 
25
18
  txt = content.inner_html
26
19
 
27
- val = get_value(data_item)
28
-
29
- txt.gsub!(to_placeholder, sanitize(val))
20
+ txt.gsub!(to_placeholder, sanitize(@data_source.value))
30
21
 
31
22
  content.inner_html = txt
32
23
 
33
24
  end
34
25
 
35
- def get_value(data_item = nil)
36
- @value || @block.call(data_item) || ''
37
- end
38
-
39
- def extract_value(data_item)
40
- return unless data_item
41
-
42
- key = @data_field || @name
43
-
44
- if data_item.is_a?(Hash)
45
- data_item[key] || data_item[key.to_s.downcase] || data_item[key.to_s.upcase] || data_item[key.to_s.downcase.to_sym]
46
-
47
- elsif data_item.respond_to?(key.to_s.downcase.to_sym)
48
- data_item.send(key.to_s.downcase.to_sym)
49
-
50
- else
51
- raise "Can't find field [#{key}] in this #{data_item.class}"
52
-
53
- end
54
-
55
- end
56
-
57
- private
26
+ private
58
27
 
59
28
  def to_placeholder
60
29
  if DELIMITERS.is_a?(Array)
@@ -82,7 +51,5 @@ module ODFReport
82
51
  s.to_s.gsub("\n", "<text:line-break/>")
83
52
  end
84
53
 
85
-
86
-
87
54
  end
88
55
  end
@@ -0,0 +1,57 @@
1
+ module ODFReport
2
+ class Image < Field
3
+
4
+ IMAGE_DIR_NAME = "Pictures"
5
+
6
+ attr_reader :files
7
+
8
+ def initialize(opts, &block)
9
+ @files = []
10
+ super
11
+ end
12
+
13
+ def replace!(doc, data_item = nil)
14
+
15
+ frame = doc.xpath("//draw:frame[@draw:name='#{@name}']").first
16
+ image = doc.xpath("//draw:frame[@draw:name='#{@name}']/draw:image").first
17
+
18
+ return unless image
19
+
20
+ file = @data_source.value
21
+
22
+ if file
23
+ image.attribute('href').content = File.join(IMAGE_DIR_NAME, File.basename(file))
24
+ frame.attribute('name').content = SecureRandom.uuid
25
+
26
+ @files << file
27
+ else
28
+ frame.remove
29
+ end
30
+
31
+ end
32
+
33
+ def self.include_image_file(zip_file, image_file)
34
+ return unless image_file
35
+
36
+ href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
37
+
38
+ zip_file.update_file(href, File.read(image_file))
39
+ end
40
+
41
+ def self.include_manifest_entry(content, image_file)
42
+ return unless image_file
43
+
44
+ return unless root_node = content.at("//manifest:manifest")
45
+
46
+ href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
47
+
48
+ entry = content.create_element('manifest:file-entry')
49
+ entry['manifest:full-path'] = href
50
+ entry['manifest:media-type'] = MIME::Types.type_for(href)[0].content_type
51
+
52
+ root_node.add_child entry
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,65 @@
1
+ module ODFReport
2
+ class Nestable
3
+
4
+ def initialize(opts)
5
+ @name = opts[:name]
6
+
7
+ @data_source = DataSource.new(opts)
8
+
9
+ @fields = []
10
+ @texts = []
11
+ @tables = []
12
+ @sections = []
13
+ @images = []
14
+
15
+ end
16
+
17
+ def set_source(data_item)
18
+ @data_source.set_source(data_item)
19
+ self
20
+ end
21
+
22
+ def add_field(name, data_field=nil, &block)
23
+ opts = { name: name, data_field: data_field }
24
+ @fields << Field.new(opts, &block)
25
+ end
26
+ alias_method :add_column, :add_field
27
+
28
+ def add_text(name, data_field=nil, &block)
29
+ opts = {name: name, data_field: data_field}
30
+ @texts << Text.new(opts, &block)
31
+ end
32
+
33
+ def add_image(name, data_field=nil, &block)
34
+ opts = {name: name, data_field: data_field}
35
+ @images << Image.new(opts, &block)
36
+ end
37
+
38
+ def add_table(table_name, collection_field, opts={})
39
+ opts.merge!(name: table_name, collection_field: collection_field)
40
+ tab = Table.new(opts)
41
+ @tables << tab
42
+
43
+ yield(tab)
44
+ end
45
+
46
+ def add_section(section_name, collection_field, opts={})
47
+ opts.merge!(name: section_name, collection_field: collection_field)
48
+ sec = Section.new(opts)
49
+ @sections << sec
50
+
51
+ yield(sec)
52
+ end
53
+
54
+ def all_images
55
+ (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten
56
+ end
57
+
58
+ def wrap_with_ns(node)
59
+ <<-XML
60
+ <root xmlns:draw="a" xmlns:xlink="b" xmlns:text="c" xmlns:table="d">#{node.to_xml}</root>
61
+ XML
62
+ end
63
+
64
+ end
65
+ end
@@ -1,20 +1,19 @@
1
1
  module ODFReport
2
2
 
3
3
  class Report
4
- include Images
5
4
 
6
- def initialize(template_name, &block)
5
+ def initialize(template_name = nil, io: nil)
7
6
 
8
- @file = ODFReport::File.new(template_name)
7
+ @template = ODFReport::Template.new(template_name, io: io)
9
8
 
10
9
  @texts = []
11
10
  @fields = []
12
11
  @tables = []
13
- @images = {}
14
- @image_names_replacements = {}
15
12
  @sections = []
16
13
 
17
- yield(self)
14
+ @images = []
15
+
16
+ yield(self) if block_given?
18
17
 
19
18
  end
20
19
 
@@ -46,49 +45,46 @@ class Report
46
45
  yield(sec)
47
46
  end
48
47
 
49
- def add_image(name, path)
50
- @images[name] = path
48
+ def add_image(image_name, value=nil)
49
+ opts = {:name => image_name, :value => value}
50
+ image = Image.new(opts)
51
+ @images << image
51
52
  end
52
53
 
53
54
  def generate(dest = nil)
54
55
 
55
- @file.update_content do |file|
56
+ @template.update_content do |file|
56
57
 
57
- file.update_files('content.xml', 'styles.xml') do |txt|
58
+ file.update_files do |doc|
58
59
 
59
- parse_document(txt) do |doc|
60
+ @sections.each { |c| c.replace!(doc) }
61
+ @tables.each { |c| c.replace!(doc) }
60
62
 
61
- @sections.each { |s| s.replace!(doc) }
62
- @tables.each { |t| t.replace!(doc) }
63
+ @texts.each { |c| c.replace!(doc) }
64
+ @fields.each { |c| c.replace!(doc) }
63
65
 
64
- @texts.each { |t| t.replace!(doc) }
65
- @fields.each { |f| f.replace!(doc) }
66
+ @images.each { |c| c.replace!(doc) }
66
67
 
67
- find_image_name_matches(doc)
68
- avoid_duplicate_image_names(doc)
68
+ end
69
69
 
70
- end
70
+ all_images.each { |i| Image.include_image_file(file, i) }
71
71
 
72
+ file.update_manifest do |content|
73
+ all_images.each { |i| Image.include_manifest_entry(content, i) }
72
74
  end
73
75
 
74
- replace_images(file)
75
-
76
76
  end
77
77
 
78
78
  if dest
79
- ::File.open(dest, "wb") {|f| f.write(@file.data) }
79
+ File.open(dest, "wb") { |f| f.write(@template.data) }
80
80
  else
81
- @file.data
81
+ @template.data
82
82
  end
83
83
 
84
84
  end
85
85
 
86
- private
87
-
88
- def parse_document(txt)
89
- doc = Nokogiri::XML(txt)
90
- yield doc
91
- txt.replace(doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML))
86
+ def all_images
87
+ @all_images ||= (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten.uniq
92
88
  end
93
89
 
94
90
  end
@@ -1,39 +1,21 @@
1
1
  module ODFReport
2
+ class Section < Nestable
2
3
 
3
- class Section
4
- include Nested
4
+ def replace!(doc)
5
5
 
6
- def initialize(opts)
7
- @name = opts[:name]
8
- @collection_field = opts[:collection_field]
9
- @collection = opts[:collection]
6
+ return unless find_section_node(doc)
10
7
 
11
- @fields = []
12
- @texts = []
13
- @tables = []
14
- @sections = []
8
+ @data_source.each do |record|
15
9
 
16
- end
17
-
18
- def replace!(doc, row = nil)
19
-
20
- return unless @section_node = find_section_node(doc)
21
-
22
- @collection = get_collection_from_item(row, @collection_field) if row
23
-
24
- @collection.each do |data_item|
25
-
26
- new_section = get_section_node
27
-
28
- @tables.each { |t| t.replace!(new_section, data_item) }
10
+ new_section = deep_clone(@section_node)
29
11
 
30
- @sections.each { |s| s.replace!(new_section, data_item) }
12
+ @tables.each { |t| t.set_source(record).replace!(new_section) }
13
+ @sections.each { |s| s.set_source(record).replace!(new_section) }
14
+ @texts.each { |t| t.set_source(record).replace!(new_section) }
15
+ @fields.each { |f| f.set_source(record).replace!(new_section) }
16
+ @images.each { |i| i.set_source(record).replace!(new_section) }
31
17
 
32
- @texts.each { |t| t.replace!(new_section, data_item) }
33
-
34
- @fields.each { |f| f.replace!(new_section, data_item) }
35
-
36
- @section_node.before(new_section)
18
+ @section_node.before(new_section.to_xml)
37
19
 
38
20
  end
39
21
 
@@ -44,23 +26,14 @@ module ODFReport
44
26
  private
45
27
 
46
28
  def find_section_node(doc)
47
-
48
- sections = doc.xpath(".//text:section[@text:name='#{@name}']")
49
-
50
- sections.empty? ? nil : sections.first
51
-
29
+ @section_node = doc.at_css("text|section[@text|name='#{@name}']")
52
30
  end
53
31
 
54
- def get_section_node
55
- node = @section_node.dup
56
-
57
- name = node.get_attribute('text:name').to_s
58
- @idx ||=0; @idx +=1
59
- node.set_attribute('text:name', "#{name}_#{@idx}")
32
+ def deep_clone(node)
33
+ Nokogiri::XML(wrap_with_ns(node)).at("text|section")
34
+ .tap { |n| n.attribute('name').content = SecureRandom.uuid }
60
35
 
61
- node
62
36
  end
63
37
 
64
38
  end
65
-
66
39
  end
@@ -1,88 +1,83 @@
1
1
  module ODFReport
2
+ class Table < Nestable
2
3
 
3
- class Table
4
- include Nested
4
+ def initialize(opts)
5
+ super(opts)
5
6
 
6
- def initialize(opts)
7
- @name = opts[:name]
8
- @collection_field = opts[:collection_field]
9
- @collection = opts[:collection]
7
+ @template_rows = []
8
+ @header = opts[:header] || false
9
+ @skip_if_empty = opts[:skip_if_empty] || false
10
+ end
10
11
 
11
- @fields = []
12
- @texts = []
13
- @tables = []
12
+ def replace!(doc)
13
+ return unless table = find_table_node(doc)
14
14
 
15
- @template_rows = []
16
- @header = opts[:header] || false
17
- @skip_if_empty = opts[:skip_if_empty] || false
18
- end
15
+ @template_rows = table.xpath("table:table-row")
19
16
 
20
- def replace!(doc, row = nil)
17
+ @header = table.xpath("table:table-header-rows").empty? ? @header : false
21
18
 
22
- return unless table = find_table_node(doc)
19
+ if @skip_if_empty && @data_source.empty?
20
+ table.remove
21
+ return
22
+ end
23
23
 
24
- @template_rows = table.xpath("table:table-row")
24
+ @data_source.each do |record|
25
25
 
26
- @header = table.xpath("table:table-header-rows").empty? ? @header : false
26
+ new_node = get_next_row
27
27
 
28
+ @tables.each { |n| n.set_source(record).replace!(new_node) }
29
+ @sections.each { |n| n.set_source(record).replace!(new_node) }
30
+ @texts.each { |n| n.set_source(record).replace!(new_node) }
31
+ @fields.each { |n| n.set_source(record).replace!(new_node) }
32
+ @images.each { |n| n.set_source(record).replace!(new_node) }
28
33
 
29
- @collection = get_collection_from_item(row, @collection_field) if row
34
+ table.add_child(new_node.to_xml)
30
35
 
31
- if @skip_if_empty && @collection.empty?
32
- table.remove
33
- return
34
- end
36
+ end
35
37
 
36
- @collection.each do |data_item|
38
+ @template_rows.each_with_index do |r, i|
39
+ r.remove if (get_start_node..template_length) === i
40
+ end
37
41
 
38
- new_node = get_next_row
42
+ end # replace
39
43
 
40
- @tables.each { |t| t.replace!(new_node, data_item) }
44
+ private
41
45
 
42
- @texts.each { |t| t.replace!(new_node, data_item) }
46
+ def get_next_row
47
+ if @template_rows.size == 1
43
48
 
44
- @fields.each { |f| f.replace!(new_node, data_item) }
49
+ ret = @template_rows.first
45
50
 
46
- table.add_child(new_node)
51
+ else
52
+ @row_cursor = get_start_node unless defined?(@row_cursor)
47
53
 
48
- end
54
+ ret = @template_rows[@row_cursor]
55
+ if @template_rows.size == @row_cursor + 1
56
+ @row_cursor = get_start_node
57
+ else
58
+ @row_cursor += 1
59
+ end
60
+ end
49
61
 
50
- @template_rows.each_with_index do |r, i|
51
- r.remove if (get_start_node..template_length) === i
62
+ return deep_clone(ret)
63
+ # return ret.dup
52
64
  end
53
65
 
54
- end # replace
55
-
56
- private
57
-
58
- def get_next_row
59
- @row_cursor = get_start_node unless defined?(@row_cursor)
60
-
61
- ret = @template_rows[@row_cursor]
62
- if @template_rows.size == @row_cursor + 1
63
- @row_cursor = get_start_node
64
- else
65
- @row_cursor += 1
66
+ def get_start_node
67
+ @header ? 1 : 0
66
68
  end
67
- return ret.dup
68
- end
69
-
70
- def get_start_node
71
- @header ? 1 : 0
72
- end
73
-
74
- def template_length
75
- @tl ||= @template_rows.size
76
- end
77
69
 
78
- def find_table_node(doc)
70
+ def template_length
71
+ @tl ||= @template_rows.size
72
+ end
79
73
 
80
- tables = doc.xpath(".//table:table[@table:name='#{@name}']")
74
+ def find_table_node(doc)
75
+ doc.at_css("table|table[@table|name='#{@name}']")
76
+ end
81
77
 
82
- tables.empty? ? nil : tables.first
78
+ def deep_clone(node)
79
+ Nokogiri::XML(wrap_with_ns(node)).at("table|table-row")
80
+ end
83
81
 
84
82
  end
85
-
86
- end
87
-
88
83
  end