caxlsx 3.0.4 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1add2e0fe915a49d02b64fe4df1aac62541f75ed27c97d0b418cdea137077c6
4
- data.tar.gz: d334797649d8ea1382ff2809359b7455cbde69365ecf798de4a51f0cfce903ca
3
+ metadata.gz: dfea731879b2415d4ed0ac46abfbad17483d7e9f2c27802aef87a93823589af7
4
+ data.tar.gz: 709606e3d36fe21ac41a8f9381d4017c31103d6ce0a8686eb195a935c3626538
5
5
  SHA512:
6
- metadata.gz: b164b9fe978fbc8866c3dbeea317c50d93c8b6796eeeb3a6b5214e9c11be439034543e1c1caaf9b4ddbf8a23713398a1bb4c6c207107a77b47353e183f59a226
7
- data.tar.gz: c3b2eb3c86f0498ce1979bc3898a22baff4ca2e3eb0b9efe4751062fb2cb69ba6b0cfdce72c7366830627b5c912450303abceeb7ca81179781a4f865bd4d8186
6
+ metadata.gz: cd1fd47bfe33dafa4909960d326f81433b4fa3e18d1a6f6f99e7a13617eecc98f212e2a938788d53466a47b52a0a2325a8dfcb3a6c3b52def5325b75ead9d402
7
+ data.tar.gz: bd9deebebd7cfc2222e64a8485ec9951ece3a69bb695fad5f4a790166324dab45134e835d02dc503e6c37e70b27d586db8bccf0c847448944f50ef37205e53d6
data/CHANGELOG.md CHANGED
@@ -1,7 +1,12 @@
1
1
  CHANGELOG
2
2
  ---------
3
3
 
4
- - **Unreleased**
4
+ - **March.27.21**: 3.1.0
5
+ - [PR #95](https://github.com/caxlsx/caxlsx/pull/95) - Replace mimemagic with marcel
6
+ - [PR #87](https://github.com/caxlsx/caxlsx/pull/87) - Implement :offset option for worksheet#add_row
7
+ - [PR #79](https://github.com/caxlsx/caxlsx/pull/79) - Add support for format in pivot tables
8
+ - [PR #77](https://github.com/caxlsx/caxlsx/pull/77) - Fix special characters in table header
9
+ - [PR #57](https://github.com/caxlsx/caxlsx/pull/57) - Deprecate using #serialize with boolean argument: Calls like `Package#serialize("name.xlsx", false)` should be replaced with `Package#serialize("name.xlsx", confirm_valid: false)`.
5
10
 
6
11
  - **January.5.21**: 3.0.4
7
12
  - [PR #72](https://github.com/caxlsx/caxlsx/pull/72) - Relax Ruby dependency to allow for Ruby 3. This required Travis to be upgraded from Ubuntu Trusty to Ubuntu Bionic. rbx-3 was dropped.
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
  [![Build Status](https://travis-ci.com/caxlsx/caxlsx.svg?branch=master)](https://travis-ci.com/caxlsx/caxlsx)
3
3
  [![Gem
4
4
  Version](https://badge.fury.io/rb/caxlsx.svg)](http://badge.fury.io/rb/caxlsx)
5
+ ![Total downloads](http://ruby-gem-downloads-badge.herokuapp.com/caxlsx?type=total)
6
+ ![Downloads for 3.0.4 (latest)](http://ruby-gem-downloads-badge.herokuapp.com/caxlsx/3.0.4?label=downloads%203.0.4)
5
7
 
6
8
  ## Notice: Community Axlsx Organization
7
9
 
data/lib/axlsx.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # encoding: UTF-8
2
2
  require 'htmlentities'
3
3
  require 'axlsx/version.rb'
4
- require 'mimemagic'
4
+ require 'marcel'
5
5
 
6
6
  require 'axlsx/util/simple_typed_list.rb'
7
7
  require 'axlsx/util/constants.rb'
data/lib/axlsx/package.rb CHANGED
@@ -74,13 +74,14 @@ module Axlsx
74
74
  # Serialize your workbook to disk as an xlsx document.
75
75
  #
76
76
  # @param [String] output The name of the file you want to serialize your package to
77
- # @param [Boolean] confirm_valid Validate the package prior to serialization.
78
- # @param [String, nil] zip_command When `nil`, `#serialize` with RubyZip to
77
+ # @param [Hash] options
78
+ # @option options [Boolean] :confirm_valid Validate the package prior to serialization.
79
+ # @option options [String] :zip_command When `nil`, `#serialize` with RubyZip to
79
80
  # zip the XLSX file contents. When a String, the provided zip command (e.g.,
80
81
  # "zip") is used to zip the file contents (may be faster for large files)
81
82
  # @return [Boolean] False if confirm_valid and validation errors exist. True if the package was serialized
82
83
  # @note A tremendous amount of effort has gone into ensuring that you cannot create invalid xlsx documents.
83
- # confirm_valid should be used in the rare case that you cannot open the serialized file.
84
+ # options[:confirm_valid] should be used in the rare case that you cannot open the serialized file.
84
85
  # @see Package#validate
85
86
  # @example
86
87
  # # This is how easy it is to create a valid xlsx file. Of course you might want to add a sheet or two, and maybe some data, styles and charts.
@@ -92,14 +93,15 @@ module Axlsx
92
93
  # p.serialize("example.xlsx")
93
94
  #
94
95
  # # Serialize to a file, using a system zip binary
95
- # p.serialize("example.xlsx", false, zip_command: "zip")
96
- # p.serialize("example.xlsx", false, zip_command: "/path/to/zip")
97
- # p.serialize("example.xlsx", false, zip_command: "zip -1")
96
+ # p.serialize("example.xlsx", zip_command: "zip", confirm_valid: false)
97
+ # p.serialize("example.xlsx", zip_command: "/path/to/zip")
98
+ # p.serialize("example.xlsx", zip_command: "zip -1")
98
99
  #
99
100
  # # Serialize to a stream
100
101
  # s = p.to_stream()
101
102
  # File.open('example_streamed.xlsx', 'w') { |f| f.write(s.read) }
102
- def serialize(output, confirm_valid=false, zip_command: nil)
103
+ def serialize(output, options = {}, secondary_options = nil)
104
+ confirm_valid, zip_command = parse_serialize_options(options, secondary_options)
103
105
  return false unless !confirm_valid || self.validate.empty?
104
106
  zip_provider = if zip_command
105
107
  ZipCommand.new(zip_command)
@@ -359,5 +361,28 @@ module Axlsx
359
361
  rels.lock
360
362
  rels
361
363
  end
364
+
365
+ # Parse the arguments of `#serialize`
366
+ # @return [Boolean, (String or nil)] Returns an array where the first value is
367
+ # `confirm_valid` and the second is the `zip_command`.
368
+ # @private
369
+ def parse_serialize_options(options, secondary_options)
370
+ if secondary_options
371
+ warn "[DEPRECATION] Axlsx::Package#serialize with 3 arguments is deprecated. " +
372
+ "Use keyword args instead e.g., package.serialize(output, confirm_valid: false, zip_command: 'zip')"
373
+ end
374
+ if options.is_a?(Hash)
375
+ options.merge!(secondary_options || {})
376
+ invalid_keys = options.keys - [:confirm_valid, :zip_command]
377
+ if invalid_keys.any?
378
+ raise ArgumentError.new("Invalid keyword arguments: #{invalid_keys}")
379
+ end
380
+ [options.fetch(:confirm_valid, false), options.fetch(:zip_command, nil)]
381
+ else
382
+ warn "[DEPRECATION] Axlsx::Package#serialize with confirm_valid as a boolean is deprecated. " +
383
+ "Use keyword args instead e.g., package.serialize(output, confirm_valid: false)"
384
+ parse_serialize_options((secondary_options || {}).merge(confirm_valid: options), nil)
385
+ end
386
+ end
362
387
  end
363
388
  end
@@ -5,7 +5,7 @@ module Axlsx
5
5
  # @param [String] v File path
6
6
  # @return [String] File mime type
7
7
  def self.get_mime_type(v)
8
- MimeMagic.by_magic(File.open(v)).to_s
8
+ Marcel::MimeType.for(Pathname.new(v))
9
9
  end
10
10
  end
11
11
  end
data/lib/axlsx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Axlsx
2
2
 
3
3
  # The current version
4
- VERSION = "3.0.4"
4
+ VERSION = "3.1.0"
5
5
  end
@@ -111,8 +111,12 @@ module Axlsx
111
111
  if data_field.is_a? String
112
112
  data_field = {:ref => data_field}
113
113
  end
114
- data_field.values.each do |value|
115
- DataTypeValidator.validate "#{self.class}.data[]", [String], value
114
+ data_field.each do |key, value|
115
+ if key == :num_fmt
116
+ DataTypeValidator.validate "#{self.class}.data[]", [Integer], value
117
+ else
118
+ DataTypeValidator.validate "#{self.class}.data[]", [String], value
119
+ end
116
120
  end
117
121
  @data << data_field
118
122
  end
@@ -212,6 +216,7 @@ module Axlsx
212
216
  data.each do |datum_value|
213
217
  # The correct name prefix in ["Sum","Average", etc...]
214
218
  str << "<dataField name='#{(datum_value[:subtotal]||'')} of #{datum_value[:ref]}' fld='#{header_index_of(datum_value[:ref])}' baseField='0' baseItem='0'"
219
+ str << " numFmtId='#{datum_value[:num_fmt]}'" if datum_value[:num_fmt]
215
220
  str << " subtotal='#{datum_value[:subtotal]}' " if datum_value[:subtotal]
216
221
  str << "/>"
217
222
  end
@@ -53,7 +53,7 @@ module Axlsx
53
53
  str << '</cacheSource>'
54
54
  str << ( '<cacheFields count="' << pivot_table.header_cells_count.to_s << '">')
55
55
  pivot_table.header_cells.each do |cell|
56
- str << ( '<cacheField name="' << cell.value << '" numFmtId="0">')
56
+ str << ( '<cacheField name="' << cell.clean_value << '" numFmtId="0">')
57
57
  str << '<sharedItems count="0">'
58
58
  str << '</sharedItems>'
59
59
  str << '</cacheField>'
@@ -25,11 +25,12 @@ module Axlsx
25
25
  # @option options [Array, Symbol] types
26
26
  # @option options [Array, Integer] style
27
27
  # @option options [Float] height the row's height (in points)
28
+ # @option options [Integer] offset - add empty columns before values
28
29
  # @see Row#array_to_cells
29
30
  # @see Cell
30
31
  def initialize(worksheet, values=[], options={})
31
32
  self.worksheet = worksheet
32
- super(Cell, nil, values.size)
33
+ super(Cell, nil, values.size + options[:offset].to_i)
33
34
  self.height = options.delete(:height)
34
35
  worksheet.rows << self
35
36
  array_to_cells(values, options)
@@ -147,14 +148,15 @@ module Axlsx
147
148
  # @option options [Array, Integer] style
148
149
  def array_to_cells(values, options={})
149
150
  DataTypeValidator.validate :array_to_cells, Array, values
150
- types, style, formula_values, escape_formulas = options.delete(:types), options.delete(:style), options.delete(:formula_values), options.delete(:escape_formulas)
151
+ types, style, formula_values, escape_formulas, offset = options.delete(:types), options.delete(:style), options.delete(:formula_values), options.delete(:escape_formulas), options.delete(:offset)
152
+ offset.to_i.times { |index| self[index] = Cell.new(self) } if offset
151
153
  values.each_with_index do |value, index|
152
154
  options[:style] = style.is_a?(Array) ? style[index] : style if style
153
155
  options[:type] = types.is_a?(Array) ? types[index] : types if types
154
156
  options[:escape_formulas] = escape_formulas.is_a?(Array) ? escape_formulas[index] : escape_formulas if escape_formulas
155
157
  options[:formula_value] = formula_values[index] if formula_values.is_a?(Array)
156
158
 
157
- self[index] = Cell.new(self, value, options)
159
+ self[index + offset.to_i] = Cell.new(self, value, options)
158
160
  end
159
161
  end
160
162
  end
@@ -80,7 +80,7 @@ module Axlsx
80
80
  str << ('<autoFilter ref="' << @ref << '"/>')
81
81
  str << ('<tableColumns count="' << header_cells.length.to_s << '">')
82
82
  header_cells.each_with_index do |cell,index|
83
- str << ('<tableColumn id ="' << (index+1).to_s << '" name="' << cell.value << '"/>')
83
+ str << ('<tableColumn id ="' << (index+1).to_s << '" name="' << cell.clean_value << '"/>')
84
84
  end
85
85
  str << '</tableColumns>'
86
86
  table_style_info.to_xml_string(str)
@@ -393,6 +393,9 @@ module Axlsx
393
393
  # @example - specify whether a certain cells in a row should escape formulas or not
394
394
  # ws.add_row ['=IF(2+2=4,4,5)', '=IF(13+13=4,4,5)'], :escape_formulas=>[true, false]
395
395
  #
396
+ # @example - add a column offset when adding a row (inserts 'n' blank, unstyled columns before data)
397
+ # ws.add_row ['I wish', 'for a fish', 'on my fish wish dish'], offset: 3
398
+ #
396
399
  # @see Worksheet#column_widths
397
400
  # @return [Row]
398
401
  # @option options [Array] values
@@ -400,6 +403,7 @@ module Axlsx
400
403
  # @option options [Array, Integer] style
401
404
  # @option options [Array] widths each member of the widths array will affect how auto_fit behavies.
402
405
  # @option options [Float] height the row's height (in points)
406
+ # @option options [Integer] offset - add empty columns before values
403
407
  # @option options [Array, Boolean] escape_formulas - Whether to treat a value starting with an equal
404
408
  # sign as formula (default) or as simple string.
405
409
  # Allowing user generated data to be interpreted as formulas can be dangerous
data/test/tc_helper.rb CHANGED
@@ -8,5 +8,3 @@ end
8
8
  require 'test/unit'
9
9
  require "timecop"
10
10
  require "axlsx.rb"
11
- # MIME detection for Microsoft Office 2007+ formats
12
- require 'mimemagic/overlay'
data/test/tc_package.rb CHANGED
@@ -129,25 +129,28 @@ class TestPackage < Test::Unit::TestCase
129
129
  def test_serialization
130
130
  @package.serialize(@fname)
131
131
  assert_zip_file_matches_package(@fname, @package)
132
+ assert_created_with_rubyzip(@fname, @package)
132
133
  File.delete(@fname)
133
134
  end
134
135
 
135
136
  def test_serialization_with_zip_command
136
- @package.serialize(@fname, false, zip_command: "zip")
137
+ @package.serialize(@fname, zip_command: "zip")
137
138
  assert_zip_file_matches_package(@fname, @package)
139
+ assert_created_with_zip_command(@fname, @package)
138
140
  File.delete(@fname)
139
141
  end
140
142
 
141
143
  def test_serialization_with_zip_command_and_absolute_path
142
144
  fname = "#{Dir.tmpdir}/#{@fname}"
143
- @package.serialize(fname, false, zip_command: "zip")
145
+ @package.serialize(fname, zip_command: "zip")
144
146
  assert_zip_file_matches_package(fname, @package)
147
+ assert_created_with_zip_command(fname, @package)
145
148
  File.delete(fname)
146
149
  end
147
150
 
148
151
  def test_serialization_with_invalid_zip_command
149
152
  assert_raises Axlsx::ZipCommand::ZipError do
150
- @package.serialize(@fname, false, zip_command: "invalid_zip")
153
+ @package.serialize(@fname, zip_command: "invalid_zip")
151
154
  end
152
155
  end
153
156
 
@@ -156,6 +159,53 @@ class TestPackage < Test::Unit::TestCase
156
159
  package.send(:parts).each{ |part| zf.get_entry(part[:entry]) }
157
160
  end
158
161
 
162
+ def assert_created_with_rubyzip(fname, package)
163
+ assert_equal 2098, get_mtime(fname, package).year, "XLSX files created with RubyZip have 2098 as the file mtime"
164
+ end
165
+
166
+ def assert_created_with_zip_command(fname, package)
167
+ assert_equal Time.now.utc.year, get_mtime(fname, package).year, "XLSX files created with a zip command have the current year as the file mtime"
168
+ end
169
+
170
+ def get_mtime(fname, package)
171
+ zf = Zip::File.open(fname)
172
+ part = package.send(:parts).first
173
+ entry = zf.get_entry(part[:entry])
174
+ entry.mtime.utc
175
+ end
176
+
177
+ def test_serialization_with_deprecated_argument
178
+ warnings = capture_warnings do
179
+ @package.serialize(@fname, false)
180
+ end
181
+ assert_equal 1, warnings.size
182
+ assert_includes warnings.first, "confirm_valid as a boolean is deprecated"
183
+ File.delete(@fname)
184
+ end
185
+
186
+ def test_serialization_with_deprecated_three_arguments
187
+ warnings = capture_warnings do
188
+ @package.serialize(@fname, true, zip_command: "zip")
189
+ end
190
+ assert_zip_file_matches_package(@fname, @package)
191
+ assert_created_with_zip_command(@fname, @package)
192
+ assert_equal 2, warnings.size
193
+ assert_includes warnings.first, "with 3 arguments is deprecated"
194
+ File.delete(@fname)
195
+ end
196
+
197
+ def capture_warnings(&block)
198
+ original_warn = Kernel.instance_method(:warn)
199
+ warnings = []
200
+ Kernel.send(:define_method, :warn) { |string| warnings << string }
201
+ block.call
202
+ original_verbose = $VERBOSE
203
+ $VERBOSE = nil
204
+ Kernel.send(:define_method, :warn, original_warn)
205
+ $VERBOSE = original_verbose
206
+ warnings
207
+ end
208
+
159
209
  # See comment for Package#zip_entry_for_part
160
210
  def test_serialization_creates_identical_files_at_any_time_if_created_at_is_set
161
211
  @package.core.created = Time.now
@@ -178,7 +228,7 @@ class TestPackage < Test::Unit::TestCase
178
228
  end
179
229
 
180
230
  def test_serialization_creates_files_with_excel_mime_type
181
- assert_equal(MimeMagic.by_magic(@package.to_stream).type,
231
+ assert_equal(Marcel::MimeType.for(@package.to_stream),
182
232
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
183
233
  end
184
234
 
@@ -132,4 +132,12 @@ class TestPivotTable < Test::Unit::TestCase
132
132
  end
133
133
  shared_test_pivot_table_xml_validity(pivot_table)
134
134
  end
135
+
136
+ def test_add_pivot_table_with_format_options_on_data_field
137
+ pivot_table = @ws.add_pivot_table('G5:G6', 'A1:E5') do |pt|
138
+ pt.data = [{:ref=>"Sales", :subtotal => 'sum', num_fmt: 4}]
139
+ end
140
+ doc = Nokogiri::XML(pivot_table.to_xml_string)
141
+ assert_equal('4', doc.at_css('dataFields dataField')['numFmtId'], 'adding format options to pivot_table')
142
+ end
135
143
  end
@@ -51,4 +51,12 @@ class TestPivotTableCacheDefinition < Test::Unit::TestCase
51
51
  assert(errors.empty?, "error free validation")
52
52
  end
53
53
 
54
+ def test_to_xml_string_for_special_characters
55
+ cell = @ws.rows.first.cells.first
56
+ cell.value = "&><'\""
57
+
58
+ doc = Nokogiri::XML(@cache_definition.to_xml_string)
59
+ errors = doc.errors
60
+ assert(errors.empty?, "invalid xml: #{errors.map(&:to_s).join(', ')}")
61
+ end
54
62
  end
@@ -136,4 +136,25 @@ class TestRow < Test::Unit::TestCase
136
136
  assert_equal(r_s_xml.xpath(".//row[@r=1][@ht=20][@customHeight=1]").size, 1)
137
137
  end
138
138
 
139
+ def test_offsets
140
+ offset = 3
141
+ values = [1,2,3,4,5]
142
+ r = @ws.add_row(values, offset: offset, style: 1)
143
+ r.cells.each_with_index do |c, index|
144
+ assert_equal(c.style, index < offset ? 0 : 1)
145
+ assert_equal(c.value, index < offset ? nil : values[index - offset])
146
+ end
147
+ end
148
+
149
+ def test_offsets_with_styles
150
+ offset = 3
151
+ values = [1,2,3,4,5]
152
+ styles = (1..5).map{ @ws.workbook.styles.add_style }
153
+ r = @ws.add_row(values, offset: offset, style: styles)
154
+ r.cells.each_with_index do |c, index|
155
+ assert_equal(c.style, index < offset ? 0 : styles[index-offset])
156
+ assert_equal(c.value, index < offset ? nil : values[index - offset])
157
+ end
158
+ end
159
+
139
160
  end
@@ -64,4 +64,14 @@ class TestTable < Test::Unit::TestCase
64
64
  end
65
65
  assert(errors.empty?, "error free validation")
66
66
  end
67
+
68
+ def test_to_xml_string_for_special_characters
69
+ cell = @ws.rows.first.cells.first
70
+ cell.value = "&><'\""
71
+
72
+ table = @ws.add_table("A1:D5")
73
+ doc = Nokogiri::XML(table.to_xml_string)
74
+ errors = doc.errors
75
+ assert(errors.empty?, "invalid xml: #{errors.map(&:to_s).join(', ')}")
76
+ end
67
77
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caxlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.4
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Randy Morgan
8
8
  - Jurriaan Pruis
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-01-05 00:00:00.000000000 Z
12
+ date: 2021-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: nokogiri
@@ -72,19 +72,19 @@ dependencies:
72
72
  - !ruby/object:Gem::Version
73
73
  version: 4.3.4
74
74
  - !ruby/object:Gem::Dependency
75
- name: mimemagic
75
+ name: marcel
76
76
  requirement: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '0.3'
80
+ version: '1.0'
81
81
  type: :runtime
82
82
  prerelease: false
83
83
  version_requirements: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: '0.3'
87
+ version: '1.0'
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: yard
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -452,7 +452,7 @@ homepage: https://github.com/caxlsx/caxlsx
452
452
  licenses:
453
453
  - MIT
454
454
  metadata: {}
455
- post_install_message:
455
+ post_install_message:
456
456
  rdoc_options: []
457
457
  require_paths:
458
458
  - lib
@@ -468,7 +468,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
468
468
  version: '0'
469
469
  requirements: []
470
470
  rubygems_version: 3.0.3
471
- signing_key:
471
+ signing_key:
472
472
  specification_version: 4
473
473
  summary: Excel OOXML (xlsx) with charts, styles, images and autowidth columns.
474
474
  test_files: