odf-report 0.5.2 → 0.7.2

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