tallty_import_export 1.0.32 → 1.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc07ae6791e3abfee248a3dd1413dad877cdc1e262bf0b93f3d79d73ba37e22e
4
- data.tar.gz: b90999b8e70f6d3ecdf96ecfa2eb086ac2ff0a99abeaec0e11f45f887fc5ce14
3
+ metadata.gz: 72c586f1a3e13625520d1ce6378e478ee8dc6cf0055a16528f63ead86c20794f
4
+ data.tar.gz: 857a44c2a36c69347a2baed91855d943a16581427bdd1b562dd02e54d4a7d97c
5
5
  SHA512:
6
- metadata.gz: fa462f47d3f0b4b09d3a759a9c64b3b3e358d4398e2e7f6667cd929dd8d0f7b5b825ea7428a8c8d6c0d26c068399e9745fc8d3137619ebc30810f8a2dc338958
7
- data.tar.gz: 7fd7387e232a4b5be306f4e1c9d5b5836ab257e78b9ed45cc3092ce4e85201a91323c9cffda041f356951f59aa02bcda3c7ff36ce8834e970b9f02d00098f0ac
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
@@ -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&.with_indifferent_access
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, 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, associations)
60
+ yield line_info.with_indifferent_access, associations
39
61
  end
40
62
  end
41
63
  end
@@ -44,7 +66,7 @@ module TalltyImportExport
44
66
  def import_data data, associations, **options
45
67
  process_options(options)
46
68
  TalltyImportExport::Excel::Rows.new(data).each_with_excel_hash(@excel_hash) do |line_info|
47
- process_line_info(line_info, associations)
69
+ process_line_info(line_info.with_indifferent_access, associations)
48
70
  end
49
71
  end
50
72
 
@@ -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
@@ -113,13 +135,16 @@ module TalltyImportExport
113
135
  # TODO: 这里,对于import_headers,后面还是不要传参数了
114
136
  # 需要合并proc,前端没有办法把proc传过来
115
137
  def import_headers_result
116
- import_header_hash = import_headers.to_h { |header| [header.with_indifferent_access[:key], header] }.with_indifferent_access
117
- @headers.map do |header|
118
- key = header[:key]
119
- if import_header_hash.fetch(key)&.fetch(:proc).present?
120
- header[:proc] = import_header_hash.fetch(key)&.fetch(:proc)
138
+ if @headers.present?
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? })
121
145
  end
122
- header
146
+ else
147
+ @headers = import_headers(**options.symbolize_keys)
123
148
  end
124
149
  rescue
125
150
  @headers
@@ -170,5 +195,27 @@ module TalltyImportExport
170
195
  associations.create!(line_info.clone.except!(*@skip_keys))
171
196
  end
172
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
173
220
  end
174
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.32"
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.32
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-11-11 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