tallty_import_export 1.0.35 → 1.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: 25de46efb361ea554c4cef13898a6f14ac03e8b56cb5ca7fcbadb72ae40dcfcd
4
- data.tar.gz: 0346d65fe26f1c9ee4357aa8b5850a86f9007c5b6da719b8b604a46ef6fe1fea
3
+ metadata.gz: 72c586f1a3e13625520d1ce6378e478ee8dc6cf0055a16528f63ead86c20794f
4
+ data.tar.gz: 857a44c2a36c69347a2baed91855d943a16581427bdd1b562dd02e54d4a7d97c
5
5
  SHA512:
6
- metadata.gz: 82b4c135ba29f61e1c1dbd4640b9a8c29639d9ad699474d2a02a11c6db8462269cfe46d9679fc6721258ab2a1b6cbfdc3fb59353de149d21f4cb541b62f206f3
7
- data.tar.gz: 54d41b0b50c542827f8350d531aff8dfb80e202b2605cc9dc3cf1d6b49f8764ef7f8b367bf52782a9401c482eb2bd9cf3c684768e73bda935c7c8a8adea5c0cb
6
+ metadata.gz: 9ae472d4119ebb57fe5066615d49001ba99a417a067d0586f25010338e7bba1d5fdda737cf8a7892483e68b079af065e21862e87225409f3a7f2e7e0683f8d5f
7
+ data.tar.gz: fd6af86be0af8b552d2d5170e86b807a301550e7b7f3e604fa0cef539fe6f4630083a4117a494071bc734f83fb29950425f6cdb147b82e130b978c0a2fc8af6f
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tallty_import_export (1.0.24)
4
+ tallty_import_export (1.0.35)
5
5
  activesupport
6
+ attr_json
6
7
  caxlsx
7
8
  redis
8
9
  redis-objects
@@ -14,36 +15,40 @@ PATH
14
15
  GEM
15
16
  remote: https://gems.ruby-china.com/
16
17
  specs:
17
- activemodel (6.1.4.1)
18
- activesupport (= 6.1.4.1)
19
- activesupport (6.1.4.1)
18
+ activemodel (7.0.2.4)
19
+ activesupport (= 7.0.2.4)
20
+ activerecord (7.0.2.4)
21
+ activemodel (= 7.0.2.4)
22
+ activesupport (= 7.0.2.4)
23
+ activesupport (7.0.2.4)
20
24
  concurrent-ruby (~> 1.0, >= 1.0.2)
21
25
  i18n (>= 1.6, < 2)
22
26
  minitest (>= 5.1)
23
27
  tzinfo (~> 2.0)
24
- zeitwerk (~> 2.3)
25
- caxlsx (3.1.1)
28
+ attr_json (1.4.0)
29
+ activerecord (>= 5.0.0, < 7.1)
30
+ caxlsx (3.2.0)
26
31
  htmlentities (~> 4.3, >= 4.3.4)
27
32
  marcel (~> 1.0)
28
33
  nokogiri (~> 1.10, >= 1.10.4)
29
34
  rubyzip (>= 1.3.0, < 3)
30
- concurrent-ruby (1.1.9)
35
+ concurrent-ruby (1.1.10)
31
36
  diff-lcs (1.4.4)
32
37
  htmlentities (4.3.4)
33
- i18n (1.8.10)
38
+ i18n (1.10.0)
34
39
  concurrent-ruby (~> 1.0)
35
40
  marcel (1.0.2)
36
- mini_portile2 (2.6.1)
37
- minitest (5.14.4)
38
- nokogiri (1.12.4)
39
- mini_portile2 (~> 2.6.1)
41
+ mini_portile2 (2.8.0)
42
+ minitest (5.15.0)
43
+ nokogiri (1.13.4)
44
+ mini_portile2 (~> 2.8.0)
40
45
  racc (~> 1.4)
41
- racc (1.5.2)
46
+ racc (1.6.0)
42
47
  rake (12.3.3)
43
- redis (4.4.0)
48
+ redis (4.6.0)
44
49
  redis-objects (1.5.1)
45
50
  redis (~> 4.2)
46
- roo (2.8.3)
51
+ roo (2.9.0)
47
52
  nokogiri (~> 1)
48
53
  rubyzip (>= 1.3.0, < 3.0.0)
49
54
  roo-xls (1.2.0)
@@ -74,7 +79,6 @@ GEM
74
79
  tallty_duck_record
75
80
  tzinfo (2.0.4)
76
81
  concurrent-ruby (~> 1.0)
77
- zeitwerk (2.4.2)
78
82
  zip-zip (0.3)
79
83
  rubyzip (>= 1.0.0)
80
84
 
@@ -0,0 +1,100 @@
1
+ module TalltyImportExport
2
+ module Attr
3
+ class ExportHeaderItem
4
+ include AttrJson::Model
5
+ attr_json_config(bad_cast: :as_nil, unknown_key: :allow)
6
+
7
+ attr_json :key, :string
8
+ attr_json :name, :string
9
+ attr_json :attr_type, :string
10
+ attr_json :format, :string
11
+ attr_json :method, :string
12
+ attr_json :chain, :string, array: true
13
+ attr_json :merge, :boolean
14
+ attr_json :json, :string
15
+ attr_json :select, ActiveModel::Type::Value.new, array: true
16
+ attr_json :source, :boolean
17
+ attr_json :proc, ActiveModel::Type::Value.new, array: true
18
+ attr_json :children, self.to_type, array: true
19
+
20
+ attr_accessor :depth, :parent_path, :seq
21
+
22
+ def calcute_flatten_value _depth, _parent_path
23
+ @depth = _depth
24
+ @parent_path = _parent_path
25
+ if children && children.count > 0
26
+ children.map do |child|
27
+ child.calcute_flatten_value(_depth + 1, [*_parent_path, self])
28
+ end.compact.flatten
29
+ else
30
+ [self]
31
+ end
32
+ end
33
+
34
+ def cluster_seqs
35
+ calcute_flatten_value(0, []).map(&:seq)
36
+ end
37
+
38
+ def seq
39
+ @seq ||= SecureRandom.uuid
40
+ end
41
+
42
+ # def living_alone?
43
+ # depth == 0 && children.present?
44
+ # end
45
+ end
46
+
47
+ class ExportHeader
48
+ include AttrJson::Model
49
+ attr_json_config(bad_cast: :as_nil, unknown_key: :allow)
50
+
51
+ attr_json :items, ExportHeaderItem.to_type, array: true
52
+
53
+ attr_reader :header_seq_to_axios
54
+
55
+ def flatten_value
56
+ items.map do |item|
57
+ item.calcute_flatten_value(0, [])
58
+ end.flatten
59
+ end
60
+
61
+ def height
62
+ flatten_value.map(&:depth).max + 1
63
+ end
64
+
65
+ def header_lines
66
+ result = []
67
+
68
+ height.times.each do |row_index|
69
+ col_index = 0
70
+
71
+ next_line = flatten_value.map do |header|
72
+ next_header = [*header.try(:parent_path), header]&.[](row_index)
73
+
74
+ @header_seq_to_axios ||= {}
75
+
76
+
77
+ if next_header
78
+ @header_seq_to_axios[next_header.seq] ||= []
79
+ @header_seq_to_axios[next_header.seq].push([col_index, row_index])
80
+ end
81
+
82
+ # 处理最后一列
83
+ if (row_index === height - 1)
84
+ @header_seq_to_axios[header.seq] ||= []
85
+ @header_seq_to_axios[header.seq].push([col_index, row_index])
86
+ end
87
+
88
+ col_index += 1
89
+
90
+ # 下一级继续递归 或 保持自己以合并
91
+ next_header || header
92
+ end
93
+
94
+ result.push(next_line)
95
+ end
96
+ result
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1 @@
1
+ require 'tallty_import_export/attr/export_header'
@@ -1,9 +1,10 @@
1
1
  module TalltyImportExport
2
2
  class Export
3
- attr_reader :klass
3
+ attr_reader :klass, :context
4
4
 
5
5
  def initialize klass
6
6
  @klass = klass
7
+ @context = Context.new({})
7
8
  end
8
9
 
9
10
  # [
@@ -16,6 +17,7 @@ module TalltyImportExport
16
17
  # { key: 'user_department_name', name: '考核人部门' },
17
18
  # { key: 'state', name: '考核状态', method: :state_zh },
18
19
  # { key: 'score', name: '考核分', source: true },
20
+ # { key: 'model_key', name: '关键字', list: [ { key: 'model_key', name: '新属性' } ]},
19
21
  # ]
20
22
  # export_headers_result / headers
21
23
  # key: 属性的英文名,可以支持user.name这样的方式
@@ -30,6 +32,7 @@ module TalltyImportExport
30
32
  # select: [{ label: '已报备', value: 'submitted'}, ...],需要转换的枚举类型
31
33
  # source: true,如果source为true,代表从association_record 进行属性查询
32
34
  # proc: proc或者lamda,支持call,传入 record 和 context
35
+ # list: 对于list布局的,进行嵌套,生成子表格
33
36
 
34
37
  def export_xlsx records, **options
35
38
  records = with_scope records
@@ -38,6 +41,7 @@ module TalltyImportExport
38
41
  options = options.with_indifferent_access
39
42
 
40
43
  Axlsx::Package.new do |pack|
44
+ pack.use_shared_strings = true
41
45
  workbook = pack.workbook
42
46
 
43
47
  if @group_by.present?
@@ -45,20 +49,21 @@ module TalltyImportExport
45
49
  records.group_by { |record| record.send(@group_by)}.each do |key, group_records|
46
50
  next unless key.present?
47
51
  @group_key = key
48
- export_workbook workbook, group_records
52
+ export_workbook workbook, group_records, **options
49
53
  end
50
54
  else
51
55
  records.group(@group_by).count.keys.each do |key|
52
56
  next unless key.present?
53
57
  @group_key = key
54
- export_workbook workbook, records.ransack("#{@group_where}" => key).result
58
+ export_workbook workbook, records.ransack("#{@group_where}" => key).result, **options
55
59
  end
56
60
  end
57
61
  else
58
- export_workbook workbook, records
62
+ export_workbook workbook, records, **options
59
63
  end
60
64
 
61
65
  file_path = File.join(Rails.root, 'public', 'export')
66
+ # file_path = File.join('/Users/mushu/', 'export')
62
67
  FileUtils.mkdir_p(file_path) unless Dir.exist?(file_path)
63
68
  file_name = "#{Time.now.strftime('%Y%m%d%H%M%S')}#{@filename}.xlsx"
64
69
  file_path_with_name = File.join(file_path, file_name)
@@ -67,14 +72,14 @@ module TalltyImportExport
67
72
  end
68
73
  end
69
74
 
70
- def export_workbook workbook, association_records
75
+ def export_workbook workbook, association_records, **options
71
76
  # excel导出样式
72
77
  alignment = { vertical: :center, horizontal: :center }
73
78
  border = { color: '969696', style: :thin }
74
79
  title1 = workbook.styles.add_style(alignment: alignment, border: border, sz: 12, b: true)
75
80
  title2 = workbook.styles.add_style(alignment: alignment, border: border, bg_color: "2a5caa", sz: 12, fg_color: "fffffb")
76
81
  title3 = workbook.styles.add_style(alignment: alignment.merge(wrap_text: true), border: border, sz: 10)
77
- headers = export_headers_result
82
+ headers = export_headers_result **options
78
83
 
79
84
  _sheet_name = respond_to?(:sheet_name) ? self.sheet_name : nil
80
85
 
@@ -93,14 +98,15 @@ module TalltyImportExport
93
98
 
94
99
  index = 0
95
100
  association_records.each do |association_record|
96
- row = []
97
101
  records = @each_method.present? ?
98
102
  (try_method(association_record, @each_method) || [nil]) :
99
103
  [association_record]
100
104
 
101
105
  records.each do |record|
106
+ row = []
107
+ formats = []
108
+ index += 1
102
109
  headers.each_with_index do |header, col_index|
103
- index += 1
104
110
  _data = header[:source] ?
105
111
  handle_data(association_record, header, index) :
106
112
  handle_data(record, header, index)
@@ -116,8 +122,9 @@ module TalltyImportExport
116
122
  end
117
123
  end
118
124
  row.push(_data)
125
+ formats.push(header[:format]&.to_sym || (_data.is_a?(String) ? :string : nil))
119
126
  end
120
- sheet.add_row row, style: title3, height: @row_height, types: headers.map{|header| header[:format]&.to_sym}
127
+ sheet.add_row row, style: title3, height: @row_height, types: formats
121
128
  last_row = row
122
129
  end
123
130
  end
@@ -141,7 +148,7 @@ module TalltyImportExport
141
148
  end
142
149
 
143
150
  def process_options options = {}
144
- options = export_options.merge(options.compact_blank!).with_indifferent_access
151
+ options = export_options.merge(options).with_indifferent_access
145
152
 
146
153
  @row_height ||= options.delete(:row_height) || 25
147
154
  @width ||= (options.delete(:width) || 20).to_f
@@ -150,14 +157,26 @@ module TalltyImportExport
150
157
  @group_where = "#{@group_by}_eq" if @group_by.present?
151
158
  @headers ||= options.delete(:headers)
152
159
  @each_method ||= options.delete(:each_method)
160
+ @params = options
161
+ context.params = @params
153
162
  end
154
163
 
155
164
  def with_scope records
156
165
  records
157
166
  end
158
167
 
159
- def export_headers_result
160
- @headers ||= export_headers
168
+ def export_headers_result **options
169
+ if @headers.present? && @group_key.blank?
170
+ headers_hash = @headers.to_h { |header| [header.with_indifferent_access[:key], header] }.with_indifferent_access
171
+ export_headers(**options.symbolize_keys).select do |_header|
172
+ _header.with_indifferent_access[:key].to_s.in?(headers_hash.keys)
173
+ end.map do |_header|
174
+ _header = _header.with_indifferent_access
175
+ _header.merge(headers_hash[_header[:key]].delete_if { |k, v| v.blank? })
176
+ end
177
+ else
178
+ @headers = export_headers(**options.symbolize_keys)
179
+ end
161
180
  end
162
181
 
163
182
  def export_headers **args
@@ -165,38 +184,44 @@ module TalltyImportExport
165
184
  end
166
185
 
167
186
  # 处理一个记录的数据
168
- def handle_data record, header, index=0
187
+ def handle_data record, header, index=0, **opts
169
188
  data =
170
189
  if header[:key] == '_index'
171
- index + 1
190
+ index
172
191
  elsif header[:method].present?
173
192
  send(header[:method], record, header)
174
193
  elsif header[:chain].present?
175
194
  try_chain(record, header[:chain])
176
- elsif header[:json]
177
- record.send(header[:json])[header[:key]]
178
195
  elsif header[:proc] && header[:proc].respond_to?(:call)
179
- header[:proc].call(record)
196
+ header[:proc].call(record, context)
180
197
  else
181
- try_method(record, header[:key])
198
+ try_method(record, header[:key], prefix: [header[:prefix], header[:json]].compact.join('.'))
182
199
  end
183
- data = handle_format(data, header)
184
- data = handle_select(data, header)
200
+ data = handle_format(data, header, **opts)
201
+ data = handle_data_type(data, **opts)
202
+ data = handle_select(data, header, **opts)
185
203
  rescue
186
204
  ''
187
205
  end
188
206
 
189
207
  def try_chain record, arr
190
- arr.reduce(record) { |r, m| r.try(m) || r.try(:[], m) }
208
+ arr.reduce(record) do |r, m|
209
+ if r.is_a?(Array)
210
+ r.try(m) || r.try(:[], m.to_i)
211
+ else
212
+ r.try(:[], m) || r.try(:[], m.to_sym) || r.try(m)
213
+ end
214
+ end
191
215
  end
192
216
 
193
- def try_method record, method
217
+ def try_method record, method, prefix: nil
218
+ prefix_arr = prefix.to_s.split(/\./)
194
219
  arr = method.to_s.split(/\./)
195
- try_chain record, arr
220
+ try_chain record, prefix_arr + arr
196
221
  end
197
222
 
198
223
  # 根据数据类型 attr_type 进行数据的格式化
199
- def handle_format data, header
224
+ def handle_format data, header, **opts
200
225
  case header[:attr_type].to_s
201
226
  when 'string'
202
227
  data.to_s
@@ -209,7 +234,30 @@ module TalltyImportExport
209
234
  end
210
235
  end
211
236
 
212
- def handle_select data, header
237
+ def handle_data_type data, **opts
238
+ if data.is_a?(Time)
239
+ data.in_time_zone.strftime('%F %H:%M')
240
+ elsif data.is_a?(Date)
241
+ data.in_time_zone.strftime('%F')
242
+ elsif data.is_a?(TrueClass)
243
+ '是'
244
+ elsif data.is_a?(FalseClass)
245
+ '否'
246
+ elsif data.is_a?(Array) && !opts[:keep_array]
247
+ if data.first.is_a?(Hash) && data.first&.dig('url').present?
248
+ # 文件array
249
+ data.map do |item|
250
+ item.dig('url')
251
+ end.compact.join("\n")
252
+ else
253
+ data.map(&:to_s).compact.join("\n")
254
+ end
255
+ else
256
+ data
257
+ end
258
+ end
259
+
260
+ def handle_select data, header, **opts
213
261
  if header[:select].present?
214
262
  select_option = header[:select].find { |option| option[:value].to_s == data.to_s }
215
263
  select_option.present? ? select_option[:label] : data
@@ -0,0 +1,134 @@
1
+ module TalltyImportExport
2
+ class ExportForm < TalltyImportExport::Export
3
+ def initialize *attrs
4
+ super(*attrs)
5
+ end
6
+
7
+ class << self
8
+ def export_template_xlsx form
9
+ export_xlsx([], {
10
+ headers: form_transfer_to_headers(form),
11
+ header_only: true
12
+ })
13
+ end
14
+ end
15
+
16
+ def export_workbook workbook, association_records, **options
17
+ # excel导出样式
18
+ alignment = { vertical: :center, horizontal: :center }
19
+ border = { color: '969696', style: :thin }
20
+ title1 = workbook.styles.add_style(alignment: alignment, border: border, sz: 12, b: true)
21
+ title2 = workbook.styles.add_style(alignment: alignment, border: border, bg_color: "2a5caa", sz: 12, fg_color: "fffffb")
22
+ title3 = workbook.styles.add_style(alignment: alignment.merge(wrap_text: true), border: border, sz: 10)
23
+ _sheet_name = (respond_to?(:sheet_name) ? self.sheet_name : nil) || options[:sheet_name]
24
+
25
+ header_obj = export_headers_result **options
26
+
27
+ workbook.add_worksheet(name: _sheet_name) do |sheet|
28
+ index = 0
29
+
30
+ if respond_to?(:first_header)
31
+ row_index = Axlsx.col_ref(headers.size - 1)
32
+ sheet.merge_cells("A1:#{row_index}1")
33
+ sheet.add_row [first_header], style: title1, height: 30
34
+ index += 1
35
+ end
36
+
37
+ header_obj.header_lines.each do |header_line|
38
+ sheet.add_row(header_line.map(&:name), style: title2, height: 25)
39
+ index += 1
40
+ end
41
+
42
+ # 合并相同 header
43
+ header_obj.header_seq_to_axios.values.each do |axios_ary|
44
+ if axios_ary.count > 1
45
+ top_right = [axios_ary.map(&:first).min, axios_ary.map(&:last).min]
46
+ bottom_left = [axios_ary.map(&:first).max, axios_ary.map(&:last).max]
47
+ sheet.merge_cells(
48
+ Axlsx::cell_r(top_right.first, top_right.last) + ':' + Axlsx::cell_r(bottom_left.first, bottom_left.last)
49
+ )
50
+ end
51
+ end
52
+
53
+ return if options[:header_only]
54
+
55
+ value_seq_to_axios = {}
56
+
57
+ formats = []
58
+ association_records.each do |association_record|
59
+ records = @each_method.present? ?
60
+ (try_method(association_record, @each_method) || [nil]) :
61
+ [association_record]
62
+
63
+ records.each do |record|
64
+ value_living_alone_col_index_to_value_count = {}
65
+
66
+ payload = TalltyImportExport::ExportPayload.new(record, header: header_obj) do |payload, header, **opts|
67
+ _data = header[:source] ?
68
+ handle_data(association_record, header, index, **opts) :
69
+ handle_data(payload, header, index, **opts)
70
+ end
71
+
72
+
73
+ # p '----------------------------------------------------------------'
74
+ # payload.lines.each { |x| p x.map { |x| x.try(:value) || '________' } }
75
+
76
+ payload.lines.each_with_index do |line, row_index|
77
+ row = []
78
+ line.each_with_index do |value, col_index|
79
+ value_living_alone_col_index_to_value_count[col_index] ||= 0
80
+
81
+ if (TalltyImportExport::ExportPayload::Value === value)
82
+ row << value.value
83
+ value_living_alone_col_index_to_value_count[col_index] += 1
84
+ unless value_seq_to_axios[value.seq]
85
+ value_seq_to_axios[value.seq] = []
86
+ end
87
+ value_seq_to_axios[value.seq] << [col_index , row_index + index]
88
+ else
89
+ row << nil
90
+ end
91
+ formats.push(
92
+ header_obj.flatten_value[col_index].format&.to_sym || (row.last.is_a?(String) ? :string : nil)
93
+ )
94
+ end
95
+ sheet.add_row(row, style: title3, height: @row_height, types: formats)
96
+ formats = []
97
+
98
+ end
99
+
100
+ # 合并仅有一个值的一列中所有格子
101
+ value_living_alone_col_index_to_value_count.each_pair do |col_index, count|
102
+ if count == 1
103
+ sheet.merge_cells(
104
+ Axlsx::cell_r(col_index, index) + ':' + Axlsx::cell_r(col_index, index + payload.max_matrix_height - 1)
105
+ )
106
+ end
107
+ end
108
+
109
+ index += payload.max_matrix_height
110
+ end
111
+ end
112
+
113
+ # 合并相同值
114
+ value_seq_to_axios.values.each do |axios_ary|
115
+ if axios_ary.count > 1
116
+ top_right = [axios_ary.map(&:first).min, axios_ary.map(&:last).min]
117
+ bottom_left = [axios_ary.map(&:first).max, axios_ary.map(&:last).max]
118
+ sheet.merge_cells(
119
+ Axlsx::cell_r(top_right.first, top_right.last) + ':' + Axlsx::cell_r(bottom_left.first, bottom_left.last)
120
+ )
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+
127
+ def export_headers_result **options
128
+ @headers = options.symbolize_keys[:headers]
129
+ super(**options)
130
+ @headers = TalltyImportExport::Attr::ExportHeader.new({ items: @headers })
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,101 @@
1
+ class TalltyImportExport::ExportPayload::Cell
2
+ attr_accessor :cell_cluster, :value, :header, :parent_path
3
+
4
+ # cell_cluster 是表格纵向 集合
5
+ def initialize header, value, context, parent_path=[], &value_handler
6
+ @value = value
7
+ @header = header
8
+ @value_handler = value_handler
9
+
10
+ # context
11
+ @context = context
12
+
13
+ value.seq_chain.each do |s|
14
+ # s = seq
15
+ @context[:header_seq_to_value_seq_to_value][header.seq] ||= {}
16
+ @context[:header_seq_to_value_seq_to_value][header.seq][s] = value # NOTE: value 对象
17
+ end
18
+
19
+ # parent
20
+ @parent_path = parent_path
21
+
22
+ @context[:parent_cell_seq_to_children_cell_value_array][parent_cell_seq] ||= []
23
+ @context[:parent_cell_seq_to_children_cell_value_array][parent_cell_seq].push(@value)
24
+
25
+ @cell_cluster = []
26
+ end
27
+
28
+ def seq
29
+ @seq ||= SecureRandom.uuid
30
+ end
31
+
32
+ def parent_cell_seq
33
+ @parent_path.last&.seq
34
+ end
35
+
36
+ def depth
37
+ @parent_path.count
38
+ end
39
+
40
+ def header_chain_seqs
41
+ [*@parent_path, self].map { |cell| cell.header.cluster_seqs }.reduce(:concat)
42
+ end
43
+
44
+ def divise
45
+ if @header.children.present? && @value.value
46
+ new_cells = @header.children.map do |child_header|
47
+ val = @value_handler.call(@value.value, child_header.as_json.symbolize_keys, keep_array: true)
48
+ if (child_header.children.present? && val.is_a?(Array))
49
+ val.map do |val|
50
+ cell = TalltyImportExport::ExportPayload::Cell.new(
51
+ child_header,
52
+ TalltyImportExport::ExportPayload::Value.new(val, @value.chain, inherited: true),
53
+ @context,
54
+ [*self.parent_path, self],
55
+ &@value_handler
56
+ )
57
+ cell
58
+ end
59
+ else
60
+ val = @value_handler.call(@value.value, child_header.as_json.symbolize_keys)
61
+ TalltyImportExport::ExportPayload::Cell.new(
62
+ child_header,
63
+ TalltyImportExport::ExportPayload::Value.new(val, @value.chain),
64
+ @context,
65
+ [*self.parent_path, self],
66
+ &@value_handler
67
+ )
68
+ end
69
+ end.flatten
70
+ new_cells.each(&:divise)
71
+ @cell_cluster.concat(new_cells.map(&:cell_cluster))
72
+ else
73
+ @cell_cluster = [self]
74
+ end
75
+
76
+ self
77
+ end
78
+
79
+ def grow_to_line
80
+ @context[:flatten_headers].
81
+ select do |header|
82
+ header_chain_seqs.include?(header.seq)
83
+ end.
84
+ map do |header|
85
+ target_value = nil
86
+
87
+ # 兄弟细胞上先查找,以解决同级兄弟细胞父一样,而导致的值覆盖
88
+ brother_cell_value_seqs = (@context[:parent_cell_seq_to_children_cell_value_array][parent_cell_seq] || []).map(&:seq)
89
+
90
+ [*brother_cell_value_seqs, *@value.seq_chain.uniq.reverse].each do |seq|
91
+ next if target_value
92
+ # seq = @value.seq
93
+ if @context[:header_seq_to_value_seq_to_value][header.seq]&.[](seq)
94
+ target_value = @context[:header_seq_to_value_seq_to_value][header.seq][seq]
95
+ end
96
+ end
97
+
98
+ target_value
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,54 @@
1
+ # 一个 最大的竖列,分子列前的竖列
2
+ class TalltyImportExport::ExportPayload::CellColumn
3
+ attr_reader :header, :payload, :context, :cell_clusters, :flatten_cells, :tallest_cell_cluster
4
+
5
+ def initialize header, value, context, &value_handler
6
+ @header = header
7
+ @value = value
8
+ @context = context
9
+ @cell_clusters = []
10
+ @value_handler = value_handler
11
+
12
+ val = @value_handler.call(@value.value, @header.as_json.symbolize_keys, keep_array: true)
13
+ if header.children && val.is_a?(Array)
14
+ @cell_clusters = val.map do |val|
15
+ TalltyImportExport::ExportPayload::Cell.new(
16
+ @header,
17
+ TalltyImportExport::ExportPayload::Value.new(val, @value.chain),
18
+ @context,
19
+ &@value_handler
20
+ )
21
+ end
22
+ else
23
+ @cell_clusters = [
24
+ TalltyImportExport::ExportPayload::Cell.new(
25
+ @header,
26
+ TalltyImportExport::ExportPayload::Value.new(
27
+ @value_handler.call(@value.value, @header.as_json.symbolize_keys),
28
+ @value.chain,
29
+ ),
30
+ @context,
31
+ &@value_handler
32
+ )
33
+ ]
34
+ end
35
+
36
+ @flatten_cells = []
37
+
38
+ cell_divise
39
+ end
40
+
41
+ def cell_divise
42
+ divised_cell_clusters = @cell_clusters.map do |cell_cluster|
43
+ cell_cluster.divise.cell_cluster
44
+ end
45
+
46
+ @flatten_cells = divised_cell_clusters.flatten
47
+
48
+ depth_to_cells_mapping = @flatten_cells.group_by { |cell| [cell.header.seq] }
49
+
50
+ @tallest_cell_cluster = depth_to_cells_mapping.values.max { |a, b| a.count <=> b.count } || []
51
+
52
+ self
53
+ end
54
+ end
@@ -0,0 +1,15 @@
1
+ class TalltyImportExport::ExportPayload::Value
2
+ attr_reader :value, :chain, :seq_chain
3
+
4
+ def initialize(val, chain=[], inherited: false)
5
+ @value = val
6
+ @parent_chain = chain
7
+
8
+ @chain = inherited ? @parent_chain : [*@parent_chain, self]
9
+ @seq_chain = @chain.map(&:seq)
10
+ end
11
+
12
+ def seq
13
+ @seq ||= SecureRandom.uuid
14
+ end
15
+ end
@@ -0,0 +1,62 @@
1
+ require 'matrix'
2
+
3
+ class TalltyImportExport::ExportPayload
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Value
7
+ autoload :CellColumn
8
+ autoload :Cell
9
+
10
+ attr_reader :value, :context, :cell_columns, :max_matrix_height
11
+
12
+ def initialize(val, header:, &value_handler)
13
+ @value = TalltyImportExport::ExportPayload::Value.new(val)
14
+ @headers = header.items # TalltyImportExport::Attr::ExportHeaderItem array
15
+ @flatten_headers = header.flatten_value
16
+ @value_handler = value_handler
17
+ @context = {
18
+ header_seq_to_value_seq_to_value: {},
19
+ parent_cell_seq_to_children_cell_value_array: {},
20
+ flatten_headers: @flatten_headers,
21
+ }
22
+ end
23
+
24
+ def lines
25
+ @cell_columns = @headers.map do |header|
26
+ TalltyImportExport::ExportPayload::CellColumn.new(
27
+ header,
28
+ self.value,
29
+ @context,
30
+ &@value_handler
31
+ )
32
+ end
33
+
34
+ # 分离的,每个初始 header 为一个矩阵
35
+ matrixes = @cell_columns.map do |cell_column|
36
+ cell_column.tallest_cell_cluster.map(&:grow_to_line)
37
+ end
38
+
39
+ # 计算所有矩阵中最高高度,用于填充
40
+ @max_matrix_height = matrixes.map(&:count).max
41
+
42
+ # 填充各个矩阵至等高
43
+ detached_matrixes = matrixes.map do |ragged_col|
44
+ col = ragged_col.uniq
45
+ while col.count < @max_matrix_height
46
+ col.push((col[0].try(:count) || 1).times.to_a)
47
+ end
48
+ Matrix[*col]
49
+ end
50
+
51
+ # 合并矩阵
52
+ result = detached_matrixes.reduce do |out, matrix|
53
+ Matrix.hstack(out, matrix)
54
+ end.to_a
55
+ # .select do |line|
56
+ # line.select { |value| TalltyImportExport::ExportPayload::Value === value }.count > 0
57
+ # end
58
+
59
+ # result.map { |x| p x.map { |xx| xx.try(:value)}}
60
+ result
61
+ end
62
+ end
@@ -0,0 +1,21 @@
1
+ module TalltyImportExport
2
+ module ExportableForm
3
+ extend ActiveSupport::Concern
4
+ include Common
5
+
6
+ included do |base|
7
+ # base.include(Common)
8
+ base.const_set('Export', Class.new(TalltyImportExport::ExportForm))
9
+ end
10
+
11
+ module ClassMethods
12
+ def export_instance
13
+ const_get('Export').new(self)
14
+ end
15
+
16
+ def export_xlsx records, **options
17
+ export_instance.export_xlsx(records, **options)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -21,13 +21,35 @@ module TalltyImportExport
21
21
 
22
22
  # xlsx_file 为 file path or file object or TalltyImportExport::Excel.new
23
23
  def import_xlsx xlsx_file, associations, **options
24
+ process_xlsx_line_info(xlsx_file, associations, **options) do |line_info, associations|
25
+ process_line_info(line_info, associations)
26
+ end
27
+ end
28
+
29
+ def exchange_to_ids xlsx_file, associations, **options
30
+ errors = []
31
+ ids = []
32
+ process_xlsx_line_info(xlsx_file, associations, **options) do |line_info, associations|
33
+ ids << exchange_line_info_to_id(line_info, associations, errors)
34
+ end
35
+
36
+ if errors.any?
37
+ error_msg = errors.map do |line_info|
38
+ "【#{@primary_keys.map { |key| line_info[key] }.join(' - ')}】"
39
+ end.join(' ')
40
+ raise RecordNotFountError.new("以下内容未找到: #{error_msg}")
41
+ end
42
+ return ids.compact.uniq
43
+ end
44
+
45
+ def process_xlsx_line_info xlsx_file, associations, **options
24
46
  @associations = associations
25
47
  # 先处理获取出来Excel每行的数据, line_info
26
48
  process_options(options)
27
49
 
28
50
  if TalltyImportExport::Excel === xlsx_file
29
51
  xlsx_file.rows.each_with_excel_hash(@excel_hash) do |line_info|
30
- process_line_info(line_info.with_indifferent_access, associations)
52
+ yield line_info.with_indifferent_access, associations
31
53
  end
32
54
  else
33
55
  file_path = xlsx_file.is_a?(String) ? xlsx_file : xlsx_file.path
@@ -35,7 +57,7 @@ module TalltyImportExport
35
57
  xlsx.each_with_pagename do |_sheetname, sheet|
36
58
  sheet.each(**@excel_hash).with_index do |line_info, index|
37
59
  next if index == 0
38
- process_line_info(line_info.with_indifferent_access, associations)
60
+ yield line_info.with_indifferent_access, associations
39
61
  end
40
62
  end
41
63
  end
@@ -53,7 +75,7 @@ module TalltyImportExport
53
75
  end
54
76
 
55
77
  def process_options options
56
- options = import_options.merge(options.compact_blank!).with_indifferent_access
78
+ options = import_options.merge(options).with_indifferent_access
57
79
  @headers = options.delete(:headers) || import_headers
58
80
  @primary_keys = options.delete(:primary_keys) || @headers.map { |header| header[:primary_key] ? header[:key].to_sym : nil }.compact
59
81
  @skip_keys = options.delete(:skip_keys) || @headers.map { |header| header[:skip] ? header[:key].to_sym : nil }.compact
@@ -83,13 +105,13 @@ module TalltyImportExport
83
105
  end
84
106
 
85
107
  def convert_data line_info
86
- line_info.with_indifferent_access.reduce({}) do |h, (k, v)|
87
- header = import_headers_result.find do |_header|
88
- _header[:key].to_sym == k.to_sym
89
- end
108
+ info = line_info.with_indifferent_access
109
+ import_headers_result.reduce({}) do |h, header|
110
+ k = header[:key]
111
+ v = info[k]
90
112
  # header[:convert] = handle_xxx
91
113
  # handle_xxx(val, processing_line_info, raw_line_info)
92
- val = header[:convert] ? send(header[:convert], v, h, line_info) : v
114
+ val = header[:convert] ? send(header[:convert], v, h, info) : v
93
115
  if header[:json]
94
116
  h[header[:json]] ||= {}
95
117
  h[header[:json]][k] = val
@@ -114,16 +136,15 @@ module TalltyImportExport
114
136
  # 需要合并proc,前端没有办法把proc传过来
115
137
  def import_headers_result
116
138
  if @headers.present?
117
- import_header_hash = import_headers.to_h { |header| [header.with_indifferent_access[:key], header] }.with_indifferent_access
118
- @headers.map do |header|
119
- key = header[:key]
120
- if import_header_hash.dig(key, :proc).present?
121
- header[:proc] = import_header_hash.dig(key, :proc)
122
- end
123
- header
139
+ headers_hash = @headers.to_h { |header| [header.with_indifferent_access[:key], header] }.with_indifferent_access
140
+ import_headers(**context.params.symbolize_keys).select do |_header|
141
+ _header.with_indifferent_access[:key].to_s.in?(headers_hash.keys)
142
+ end.map do |_header|
143
+ _header = _header.with_indifferent_access
144
+ _header.merge(headers_hash[_header[:key]].delete_if { |k, v| v.blank? })
124
145
  end
125
146
  else
126
- @headers = import_headers
147
+ @headers = import_headers(**options.symbolize_keys)
127
148
  end
128
149
  rescue
129
150
  @headers
@@ -174,5 +195,27 @@ module TalltyImportExport
174
195
  associations.create!(line_info.clone.except!(*@skip_keys))
175
196
  end
176
197
  end
198
+
199
+ def exchange_line_info_to_id line_info, associations, errors
200
+ # 去除空行内容
201
+ return unless line_info.values.any?(&:present?)
202
+ context.line_info = line_info
203
+
204
+ return unless primary_keys.present?
205
+ ids = associations.where(line_info.clone.extract!(*primary_keys)).pluck(:id)
206
+
207
+ context.last_line_info = line_info
208
+
209
+ errors << line_info unless ids[0]
210
+ return ids[0]
211
+ end
212
+
213
+ class RecordNotFountError < StandardError
214
+ attr_accessor :message
215
+ def initialize message
216
+ @message = message
217
+ super()
218
+ end
219
+ end
177
220
  end
178
221
  end
@@ -24,6 +24,10 @@ module TalltyImportExport
24
24
  def import_excel_klass
25
25
  import_instance.tallty_excel
26
26
  end
27
+
28
+ def exchange_to_ids xlsx_file, associations, **options
29
+ import_instance.exchange_to_ids(xlsx_file, associations, **options)
30
+ end
27
31
  end
28
32
  end
29
33
  end
@@ -1,3 +1,3 @@
1
1
  module TalltyImportExport
2
- VERSION = "1.0.35"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -1,20 +1,27 @@
1
+ require 'active_support'
1
2
  require "active_support/core_ext/hash"
3
+ require "active_support/core_ext/object"
2
4
  require "tallty_import_export/version"
3
5
  require 'axlsx'
4
6
  require 'redis'
5
- require 'tallty_form'
7
+ # require 'tallty_form'
8
+ require 'attr_json'
6
9
 
7
10
  module TalltyImportExport
8
11
  extend ActiveSupport::Autoload
9
12
 
10
13
  autoload :Common
11
14
  autoload :Exportable
15
+ autoload :ExportableForm
12
16
  autoload :Export
13
17
  autoload :Importable
14
18
  autoload :Import
15
19
  autoload :Context
16
20
  autoload :FormConvert
17
21
  autoload :Excel
22
+ autoload :Attr
23
+ autoload :ExportPayload
24
+ autoload :ExportForm
18
25
 
19
26
  class Error < StandardError; end
20
27
 
@@ -14,8 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.platform = Gem::Platform::RUBY
16
16
 
17
- # Specify which files should be added to the gem when it is released.
18
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ # Specify which files should be added to the gem when it is released. The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
18
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
19
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
20
  end
@@ -31,4 +30,5 @@ Gem::Specification.new do |spec|
31
30
  spec.add_dependency('activesupport')
32
31
  spec.add_dependency('redis')
33
32
  spec.add_dependency('redis-objects')
33
+ spec.add_dependency 'attr_json'
34
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tallty_import_export
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.35
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - liyijie
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-10 00:00:00.000000000 Z
11
+ date: 2022-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zip-zip
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: attr_json
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  description: " import & export for active_record with simple_controller "
126
140
  email:
127
141
  - liyijie825@gmail.com
@@ -140,11 +154,19 @@ files:
140
154
  - bin/console
141
155
  - bin/setup
142
156
  - lib/tallty_import_export.rb
157
+ - lib/tallty_import_export/attr.rb
158
+ - lib/tallty_import_export/attr/export_header.rb
143
159
  - lib/tallty_import_export/common.rb
144
160
  - lib/tallty_import_export/context.rb
145
161
  - lib/tallty_import_export/excel.rb
146
162
  - lib/tallty_import_export/export.rb
163
+ - lib/tallty_import_export/export_form.rb
164
+ - lib/tallty_import_export/export_payload.rb
165
+ - lib/tallty_import_export/export_payload/cell.rb
166
+ - lib/tallty_import_export/export_payload/cell_column.rb
167
+ - lib/tallty_import_export/export_payload/value.rb
147
168
  - lib/tallty_import_export/exportable.rb
169
+ - lib/tallty_import_export/exportable_form.rb
148
170
  - lib/tallty_import_export/form_convert.rb
149
171
  - lib/tallty_import_export/import.rb
150
172
  - lib/tallty_import_export/importable.rb