tallty_import_export 1.0.3
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +89 -0
- data/LICENSE.txt +21 -0
- data/README.md +40 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/tallty_import_export.rb +28 -0
- data/lib/tallty_import_export/common.rb +22 -0
- data/lib/tallty_import_export/context.rb +6 -0
- data/lib/tallty_import_export/excel.rb +84 -0
- data/lib/tallty_import_export/export.rb +185 -0
- data/lib/tallty_import_export/exportable.rb +21 -0
- data/lib/tallty_import_export/form_convert.rb +15 -0
- data/lib/tallty_import_export/import.rb +145 -0
- data/lib/tallty_import_export/importable.rb +29 -0
- data/lib/tallty_import_export/version.rb +3 -0
- data/tallty_import_export.gemspec +34 -0
- metadata +176 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4a9e60e31f0ac0c3d979739c50f9631e4a678e61aa86325cd655bd40b45bc7d1
|
|
4
|
+
data.tar.gz: a75eb5bb8b865c3d1edd470a5704d1bdc0fda9002411dde9816ea1cd3ab44e98
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1d7adcd134a5ead8f4c0eefa8812f5f5578bf2f01dfd396c02c4ce45544e94f97b5397d8e81da5c320117b7034534c65cc0c54569fc66c884d7e4e03bad5e380
|
|
7
|
+
data.tar.gz: 3c36721ef2be7db0df34064fa5b02abd55b3b808644ea8f53f3e982ed90d58a60326ab62ceee365ed351faf0440180bf9d68d637b0a442cc31e4215245dab7ba
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
tallty_import_export (0.1.0)
|
|
5
|
+
activesupport
|
|
6
|
+
caxlsx
|
|
7
|
+
redis
|
|
8
|
+
redis-objects
|
|
9
|
+
roo
|
|
10
|
+
roo-xls
|
|
11
|
+
tallty_form
|
|
12
|
+
zip-zip
|
|
13
|
+
|
|
14
|
+
GEM
|
|
15
|
+
remote: https://gems.ruby-china.com/
|
|
16
|
+
specs:
|
|
17
|
+
activemodel (6.0.3.4)
|
|
18
|
+
activesupport (= 6.0.3.4)
|
|
19
|
+
activesupport (6.0.3.4)
|
|
20
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
21
|
+
i18n (>= 0.7, < 2)
|
|
22
|
+
minitest (~> 5.1)
|
|
23
|
+
tzinfo (~> 1.1)
|
|
24
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
25
|
+
caxlsx (3.0.2)
|
|
26
|
+
htmlentities (~> 4.3, >= 4.3.4)
|
|
27
|
+
mimemagic (~> 0.3)
|
|
28
|
+
nokogiri (~> 1.10, >= 1.10.4)
|
|
29
|
+
rubyzip (>= 1.3.0, < 3)
|
|
30
|
+
concurrent-ruby (1.1.7)
|
|
31
|
+
diff-lcs (1.4.4)
|
|
32
|
+
htmlentities (4.3.4)
|
|
33
|
+
i18n (1.8.5)
|
|
34
|
+
concurrent-ruby (~> 1.0)
|
|
35
|
+
mimemagic (0.3.5)
|
|
36
|
+
mini_portile2 (2.4.0)
|
|
37
|
+
minitest (5.14.2)
|
|
38
|
+
nokogiri (1.10.10)
|
|
39
|
+
mini_portile2 (~> 2.4.0)
|
|
40
|
+
rake (12.3.3)
|
|
41
|
+
redis (4.2.5)
|
|
42
|
+
redis-objects (1.5.0)
|
|
43
|
+
redis (~> 4.0)
|
|
44
|
+
roo (2.8.3)
|
|
45
|
+
nokogiri (~> 1)
|
|
46
|
+
rubyzip (>= 1.3.0, < 3.0.0)
|
|
47
|
+
roo-xls (1.2.0)
|
|
48
|
+
nokogiri
|
|
49
|
+
roo (>= 2.0.0, < 3)
|
|
50
|
+
spreadsheet (> 0.9.0)
|
|
51
|
+
rspec (3.10.0)
|
|
52
|
+
rspec-core (~> 3.10.0)
|
|
53
|
+
rspec-expectations (~> 3.10.0)
|
|
54
|
+
rspec-mocks (~> 3.10.0)
|
|
55
|
+
rspec-core (3.10.0)
|
|
56
|
+
rspec-support (~> 3.10.0)
|
|
57
|
+
rspec-expectations (3.10.0)
|
|
58
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
59
|
+
rspec-support (~> 3.10.0)
|
|
60
|
+
rspec-mocks (3.10.0)
|
|
61
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
62
|
+
rspec-support (~> 3.10.0)
|
|
63
|
+
rspec-support (3.10.0)
|
|
64
|
+
ruby-ole (1.2.12.2)
|
|
65
|
+
rubyzip (2.3.0)
|
|
66
|
+
spreadsheet (1.2.6)
|
|
67
|
+
ruby-ole (>= 1.0)
|
|
68
|
+
tallty_duck_record (1.0.2)
|
|
69
|
+
activemodel (~> 6.0.3)
|
|
70
|
+
activesupport (~> 6.0.3)
|
|
71
|
+
tallty_form (1.0.0)
|
|
72
|
+
tallty_duck_record
|
|
73
|
+
thread_safe (0.3.6)
|
|
74
|
+
tzinfo (1.2.8)
|
|
75
|
+
thread_safe (~> 0.1)
|
|
76
|
+
zeitwerk (2.4.1)
|
|
77
|
+
zip-zip (0.3)
|
|
78
|
+
rubyzip (>= 1.0.0)
|
|
79
|
+
|
|
80
|
+
PLATFORMS
|
|
81
|
+
ruby
|
|
82
|
+
|
|
83
|
+
DEPENDENCIES
|
|
84
|
+
rake (~> 12.0)
|
|
85
|
+
rspec (~> 3.0)
|
|
86
|
+
tallty_import_export!
|
|
87
|
+
|
|
88
|
+
BUNDLED WITH
|
|
89
|
+
2.1.4
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 liyijie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# TalltyImportExport
|
|
2
|
+
|
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/tallty_import_export`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
4
|
+
|
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'tallty_import_export'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle install
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install tallty_import_export
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
|
+
|
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tallty_import_export.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "tallty_import_export"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "active_support/core_ext/hash"
|
|
2
|
+
require "tallty_import_export/version"
|
|
3
|
+
require 'axlsx'
|
|
4
|
+
require 'redis'
|
|
5
|
+
require 'tallty_form'
|
|
6
|
+
|
|
7
|
+
module TalltyImportExport
|
|
8
|
+
extend ActiveSupport::Autoload
|
|
9
|
+
|
|
10
|
+
autoload :Common
|
|
11
|
+
autoload :Exportable
|
|
12
|
+
autoload :Export
|
|
13
|
+
autoload :Importable
|
|
14
|
+
autoload :Import
|
|
15
|
+
autoload :Context
|
|
16
|
+
autoload :FormConvert
|
|
17
|
+
autoload :Excel
|
|
18
|
+
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
def self.redis
|
|
22
|
+
@redis ||= Redis.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.redis=(redis)
|
|
26
|
+
@redis = redis
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
module Common
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def model_headers
|
|
10
|
+
columns.map do |column|
|
|
11
|
+
{
|
|
12
|
+
key: column.name,
|
|
13
|
+
name: column.comment || column.name,
|
|
14
|
+
attr_type: column.type,
|
|
15
|
+
format: column.type == :string ? :string : nil,
|
|
16
|
+
primary_key: column.name == 'id' ? true : false
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
class Excel
|
|
3
|
+
require 'redis-objects'
|
|
4
|
+
require 'roo'
|
|
5
|
+
|
|
6
|
+
attr_reader :uid, :cache
|
|
7
|
+
|
|
8
|
+
def initialize uid = SecureRandom.hex(8)
|
|
9
|
+
@uid = uid
|
|
10
|
+
@cache = ::Redis::HashKey.new(@uid, expiration: 60 * 60 * 1, marshal: true)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def load file, **opts
|
|
14
|
+
opts = opts.with_indifferent_access
|
|
15
|
+
|
|
16
|
+
file_path = file.is_a?(String) ? file : file.path
|
|
17
|
+
book = ::Roo::Spreadsheet.open(file_path, extension: File.extname(file_path) == '.xls' ? :xls : :xlsx)
|
|
18
|
+
|
|
19
|
+
read_title_from_row = opts[:read_title_from_row] || 1
|
|
20
|
+
read_data_from_row = opts[:read_data_from_row] || 2
|
|
21
|
+
titles = book.row(read_title_from_row)
|
|
22
|
+
rows = []
|
|
23
|
+
|
|
24
|
+
read_data_from_row.upto(book.last_row) do |i|
|
|
25
|
+
row = book.row(i)[0 .. titles.count]
|
|
26
|
+
out = {}
|
|
27
|
+
titles.each_with_index do |title, index|
|
|
28
|
+
out[title] = row[index]
|
|
29
|
+
end
|
|
30
|
+
rows << out
|
|
31
|
+
end
|
|
32
|
+
cache['uid'] = uid
|
|
33
|
+
cache['rows'] = rows
|
|
34
|
+
cache['titles'] = titles
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rows
|
|
38
|
+
Rows.new(cache['rows'])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def titles
|
|
42
|
+
cache['titles']
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def data
|
|
46
|
+
cache.all
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def records_pagination page: 1, per_page: 15
|
|
50
|
+
Pagination.new(rows, page: page, per_page: per_page)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Rows < Array
|
|
54
|
+
# excel_hash { key => excel name }
|
|
55
|
+
def each_with_excel_hash excel_hash
|
|
56
|
+
mapping = excel_hash.invert
|
|
57
|
+
each do |row|
|
|
58
|
+
line_info = row.reduce({}) do |out, (k, v)|
|
|
59
|
+
next out unless mapping[k]
|
|
60
|
+
out[mapping[k]] = v
|
|
61
|
+
out
|
|
62
|
+
end
|
|
63
|
+
yield line_info
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Pagination < Array
|
|
69
|
+
attr_reader :current_page, :total_pages
|
|
70
|
+
|
|
71
|
+
def initialize ary, page: ,per_page:
|
|
72
|
+
@raw_ary = ary
|
|
73
|
+
@current_page = page.to_i
|
|
74
|
+
@total_pages = (@raw_ary.count / per_page.to_f).ceil
|
|
75
|
+
parsed_ary = @raw_ary[(page.to_i - 1) * per_page.to_i ... (page.to_i) * per_page.to_i]
|
|
76
|
+
super(parsed_ary)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def count
|
|
80
|
+
@raw_ary.count
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
class Export
|
|
3
|
+
attr_reader :klass
|
|
4
|
+
|
|
5
|
+
def initialize klass
|
|
6
|
+
@klass = klass
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# [
|
|
10
|
+
# { key: 'entry_user_name', name: '被考核人', chain: [:entry, :user_name] },
|
|
11
|
+
# { key: 'entry_user_code', name: '被考核人工号' },
|
|
12
|
+
# { key: 'entry_user_department_name', name: '被考核人部门' },
|
|
13
|
+
# { key: 'dimension_name', name: '考核维度' },
|
|
14
|
+
# { key: 'user_name', name: '考核人' },
|
|
15
|
+
# { key: 'user_code', name: '考核人工号' },
|
|
16
|
+
# { key: 'user_department_name', name: '考核人部门' },
|
|
17
|
+
# { key: 'state', name: '考核状态', method: :state_zh },
|
|
18
|
+
# { key: 'score', name: '考核分' },
|
|
19
|
+
# ]
|
|
20
|
+
# export_headers_result / headers
|
|
21
|
+
# key: 属性的英文名,可以支持user.name这样的方式
|
|
22
|
+
# name: 属性的中文名
|
|
23
|
+
# attr_type: 属性的类型
|
|
24
|
+
# format: excel是否需要特定的格式,目前主要是类似于身份证号,可以用string
|
|
25
|
+
# method: 导出时本地调用的方法
|
|
26
|
+
# chain: 导出时对象属性通过链式调用
|
|
27
|
+
# index: 数组方式,需要嵌套拿到里面的
|
|
28
|
+
# merge: true/false,默认false,某一列,如果上下行的内容相同,则直接合并单元格
|
|
29
|
+
|
|
30
|
+
def export_xlsx records, **options
|
|
31
|
+
records = with_scope records
|
|
32
|
+
process_options(options)
|
|
33
|
+
|
|
34
|
+
options = options.with_indifferent_access
|
|
35
|
+
|
|
36
|
+
Axlsx::Package.new do |pack|
|
|
37
|
+
workbook = pack.workbook
|
|
38
|
+
|
|
39
|
+
if @group_by.present?
|
|
40
|
+
if records.is_a?(Array)
|
|
41
|
+
records.group_by { |record| record.send(@group_by)}.each do |key, group_records|
|
|
42
|
+
@group_key = key
|
|
43
|
+
export_workbook workbook, group_records
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
records.group(@group_by).count.keys.each do |key|
|
|
47
|
+
@group_key = key
|
|
48
|
+
export_workbook workbook, records.ransack("#{@group_where}" => key).result
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
export_workbook workbook, records
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
file_path = File.join(Rails.root, 'public', 'export')
|
|
56
|
+
FileUtils.mkdir_p(file_path) unless Dir.exist?(file_path)
|
|
57
|
+
file_name = "#{Time.now.strftime('%Y%m%d%H%M%S')}#{@filename}.xlsx"
|
|
58
|
+
file_path_with_name = File.join(file_path, file_name)
|
|
59
|
+
pack.serialize(file_path_with_name)
|
|
60
|
+
return file_path_with_name
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def export_workbook workbook, records
|
|
65
|
+
# excel导出样式
|
|
66
|
+
alignment = { vertical: :center, horizontal: :center }
|
|
67
|
+
border = { color: '969696', style: :thin }
|
|
68
|
+
title1 = workbook.styles.add_style(alignment: alignment, border: border, sz: 12, b: true)
|
|
69
|
+
title2 = workbook.styles.add_style(alignment: alignment, border: border, bg_color: "2a5caa", sz: 12, fg_color: "fffffb")
|
|
70
|
+
title3 = workbook.styles.add_style(alignment: alignment.merge(wrap_text: true), border: border, sz: 10)
|
|
71
|
+
headers = export_headers_result
|
|
72
|
+
|
|
73
|
+
_sheet_name = respond_to?(:sheet_name) ? self.sheet_name : nil
|
|
74
|
+
|
|
75
|
+
workbook.add_worksheet(name: _sheet_name) do |sheet|
|
|
76
|
+
if respond_to?(:first_header)
|
|
77
|
+
row_index = Axlsx.col_ref(headers.size - 1)
|
|
78
|
+
sheet.merge_cells("A1:#{row_index}1")
|
|
79
|
+
sheet.add_row [first_header], style: title1, height: 40
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
sheet.add_row headers.map{|header| header[:name]}, style: title2, height: 39
|
|
83
|
+
|
|
84
|
+
last_row = nil
|
|
85
|
+
merge_column_hash = {}
|
|
86
|
+
first_content_row_index = respond_to?(:first_header) ? 2 : 1
|
|
87
|
+
|
|
88
|
+
each_method = records.is_a?(Array) ? :each : :each
|
|
89
|
+
records.send(each_method).with_index do |record, index|
|
|
90
|
+
row = []
|
|
91
|
+
headers.each_with_index do |header, col_index|
|
|
92
|
+
_data = handle_data(record, header, index)
|
|
93
|
+
if header[:merge].present? && last_row.present? && _data == last_row[col_index]
|
|
94
|
+
# 这里使用二维数组,每个数组里都是列内容相同的各行
|
|
95
|
+
merge_column_hash[col_index] ||= []
|
|
96
|
+
if merge_column_hash[col_index].last&.last == index + first_content_row_index - 1
|
|
97
|
+
# 说明内容和上面的是延续的,继续加入之前的数组
|
|
98
|
+
merge_column_hash[col_index].last << index + first_content_row_index
|
|
99
|
+
else
|
|
100
|
+
merge_column_hash[col_index] << [index + first_content_row_index - 1, index + first_content_row_index]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
row.push(_data)
|
|
104
|
+
end
|
|
105
|
+
sheet.add_row row, style: title3, height: @row_height, types: headers.map{|header| header[:format]&.to_sym}
|
|
106
|
+
last_row = row
|
|
107
|
+
end
|
|
108
|
+
# 需要根据column进行多行的内容合并
|
|
109
|
+
if merge_column_hash.present?
|
|
110
|
+
merge_column_hash.each do |col_index, row_arr|
|
|
111
|
+
row_arr.each do |arr|
|
|
112
|
+
sheet.merge_cells(
|
|
113
|
+
Axlsx::cell_r(col_index, arr.first) + ':' + Axlsx::cell_r(col_index, arr.last)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
sheet.column_widths(*headers.map{|header| (header[:width] || @width).to_f})
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def process_options options = {}
|
|
123
|
+
options = options.with_indifferent_access
|
|
124
|
+
|
|
125
|
+
@row_height ||= options.delete(:row_height) || 35
|
|
126
|
+
@width ||= (options.delete(:width) || 20).to_f
|
|
127
|
+
@filename ||= options.delete(:filename)
|
|
128
|
+
@group_by ||= options.delete(:group_by)
|
|
129
|
+
@group_where = "#{@group_by}_eq" if @group_by.present?
|
|
130
|
+
@headers ||= options.delete(:headers)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def with_scope records
|
|
134
|
+
records
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def export_headers_result
|
|
138
|
+
@headers || export_headers
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def export_headers
|
|
142
|
+
@headers || klass.try(:headers) || klass.try(:model_headers)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# 处理一个记录的数据
|
|
146
|
+
def handle_data record, header, index=0
|
|
147
|
+
data =
|
|
148
|
+
if header[:key] == '_index'
|
|
149
|
+
index + 1
|
|
150
|
+
elsif header[:method].present?
|
|
151
|
+
send(header[:method], record, header)
|
|
152
|
+
elsif header[:chain].present?
|
|
153
|
+
try_chain(record, header[:chain])
|
|
154
|
+
elsif header[:json]
|
|
155
|
+
record.send(header[:json])[header[:key]]
|
|
156
|
+
else
|
|
157
|
+
try_method(record, header[:key])
|
|
158
|
+
end
|
|
159
|
+
handle_format(data, header)
|
|
160
|
+
rescue
|
|
161
|
+
''
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def try_chain record, arr
|
|
165
|
+
arr.inject(record, :try)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def try_method record, method
|
|
169
|
+
arr = method.to_s.split(/\./)
|
|
170
|
+
try_chain record, arr
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# 根据数据类型 attr_type 进行数据的格式化
|
|
174
|
+
def handle_format data, header
|
|
175
|
+
case header[:attr_type].to_s
|
|
176
|
+
when 'datetime'
|
|
177
|
+
data ? data.strftime('%F %H:%M') : nil
|
|
178
|
+
when 'date'
|
|
179
|
+
data ? data.strftime('%F') : nil
|
|
180
|
+
else
|
|
181
|
+
data
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
module Exportable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
include Common
|
|
5
|
+
|
|
6
|
+
included do |base|
|
|
7
|
+
# base.include(Common)
|
|
8
|
+
base.const_set('Export', Class.new(TalltyImportExport::Export))
|
|
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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
class FormConvert
|
|
3
|
+
class << self
|
|
4
|
+
def headers form, column_name: :payload
|
|
5
|
+
form.fields.map do |field|
|
|
6
|
+
{}.tap do |q|
|
|
7
|
+
q[:key] = field.key
|
|
8
|
+
q[:name] = field.name
|
|
9
|
+
q[:json] = column_name.to_sym
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
class Import
|
|
3
|
+
require 'roo'
|
|
4
|
+
attr_reader :klass, :context, :headers, :primary_keys, :associations
|
|
5
|
+
|
|
6
|
+
def initialize klass
|
|
7
|
+
@klass = klass
|
|
8
|
+
@headers = import_headers_result.map { |header| header.with_indifferent_access }
|
|
9
|
+
@context = Context.new({})
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# key: 属性的英文名
|
|
13
|
+
# name: 属性的中文名
|
|
14
|
+
# attr_type: 属性的类型
|
|
15
|
+
# format: excel是否需要特定的格式,目前主要是类似于身份证号,可以用string
|
|
16
|
+
# convert: 导入时候,把excel的内容转换成导入时候代码逻辑需要的内容
|
|
17
|
+
# primary_key: 是否是主键
|
|
18
|
+
|
|
19
|
+
# xlsx_file 为 file path or file object or TalltyImportExport::Excel.new
|
|
20
|
+
def import_xlsx xlsx_file, associations, **options
|
|
21
|
+
@associations = associations
|
|
22
|
+
# 先处理获取出来Excel每行的数据, line_info
|
|
23
|
+
process_options(options)
|
|
24
|
+
|
|
25
|
+
if TalltyImportExport::Excel === xlsx_file
|
|
26
|
+
xlsx_file.rows.each_with_excel_hash(@excel_hash) do |line_info|
|
|
27
|
+
process_line_info(line_info, associations)
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
file_path = xlsx_file.is_a?(String) ? xlsx_file : xlsx_file.path
|
|
31
|
+
xlsx = ::Roo::Excelx.new(file_path)
|
|
32
|
+
xlsx.each_with_pagename do |_sheetname, sheet|
|
|
33
|
+
sheet.each(**@excel_hash).with_index do |line_info, index|
|
|
34
|
+
next if index == 0
|
|
35
|
+
process_line_info(line_info, associations)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def import_data data, associations, **options
|
|
42
|
+
process_options(options)
|
|
43
|
+
TalltyImportExport::Excel::Rows.new(data).each_with_excel_hash(@excel_hash) do |line_info|
|
|
44
|
+
process_line_info(line_info, associations)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tallty_excel
|
|
49
|
+
TalltyImportExport::Excel
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def process_options options
|
|
53
|
+
options = options.with_indifferent_access
|
|
54
|
+
self.headers = options.delete(:headers) || []
|
|
55
|
+
@primary_keys = options.delete(:primary_keys) || headers.map { |header| header[:primary_key] ? header[:key].to_sym : nil }.compact
|
|
56
|
+
|
|
57
|
+
@excel_hash = headers.reduce({}) do |h, header|
|
|
58
|
+
h[header[:key].to_sym] = header[:name]
|
|
59
|
+
h
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
options
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def process_line_info line_info, associations
|
|
66
|
+
# 转换处理导入的数据格式
|
|
67
|
+
line_info = convert_data(line_info)
|
|
68
|
+
|
|
69
|
+
# 处理每行对于导入的动作,处理line_info
|
|
70
|
+
return unless valid?(line_info)
|
|
71
|
+
|
|
72
|
+
import_record(line_info, associations)
|
|
73
|
+
context.last_line_info = line_info
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def convert_data line_info
|
|
77
|
+
line_info.with_indifferent_access.reduce({}) do |h, (k, v)|
|
|
78
|
+
header = headers.find do |_header|
|
|
79
|
+
_header[:key].to_sym == k.to_sym
|
|
80
|
+
end
|
|
81
|
+
# header[:convert] = handle_xxx
|
|
82
|
+
# handle_xxx(val, processing_line_info, raw_line_info)
|
|
83
|
+
val = header[:convert] ? send(header[:convert], v, h, line_info) : v
|
|
84
|
+
if header[:json]
|
|
85
|
+
h[header[:json]] ||= {}
|
|
86
|
+
h[header[:json]][k] = val
|
|
87
|
+
else
|
|
88
|
+
h[k.to_sym] = val
|
|
89
|
+
end
|
|
90
|
+
h
|
|
91
|
+
end.with_indifferent_access
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 通过转换后,数据是否合法,如果不合法,则直接跳过不处理这个数据
|
|
95
|
+
def valid? line_info
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def import_headers_result
|
|
100
|
+
@headers || import_headers
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def import_headers
|
|
104
|
+
@headers || klass.try(:import_headers) || klass.try(:model_headers) || (raise ArgumentError.new('missing import_headers'))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 只保留 key, name, json, 合并到 import_header
|
|
108
|
+
def headers= val
|
|
109
|
+
if val.empty?
|
|
110
|
+
@headers = import_headers_result.map { |header| header.with_indifferent_access }
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
key_to_coming_header = val.reduce({}) do |out, header|
|
|
115
|
+
out[header.with_indifferent_access[:key].to_sym] = header.with_indifferent_access
|
|
116
|
+
out
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result = []
|
|
120
|
+
val.map do |header|
|
|
121
|
+
if (exist_header = import_headers_result.find { |model_header| model_header[:key] === header[:key] })
|
|
122
|
+
result.push(exist_header.merge(name: header[:name], json: header[:json]))
|
|
123
|
+
else
|
|
124
|
+
result.push({ key: header[:key], name: header[:name], json: header[:json]})
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@headers = result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def skip val, processing_line_info, raw_line_info
|
|
132
|
+
# do nothing there, use for header[:convert]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
### 这个方法是可以由复杂业务进行重载的 ###
|
|
136
|
+
def import_record line_info, associations
|
|
137
|
+
if primary_keys.present?
|
|
138
|
+
_record = associations.find_or_initialize_by(line_info.clone.extract!(*primary_keys))
|
|
139
|
+
_record.update!(line_info.clone.except!(*primary_keys))
|
|
140
|
+
else
|
|
141
|
+
associations.create!(line_info)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module TalltyImportExport
|
|
2
|
+
module Importable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
include Common
|
|
5
|
+
|
|
6
|
+
included do |base|
|
|
7
|
+
# base.include(Common)
|
|
8
|
+
base.const_set('Import', Class.new(TalltyImportExport::Import))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module ClassMethods
|
|
12
|
+
def import_instance
|
|
13
|
+
const_get('Import').new(self)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def import_xlsx xlsx_file, associations, **opts
|
|
17
|
+
import_instance.import_xlsx(xlsx_file, associations, **opts)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def import_data data, associations, **opts
|
|
21
|
+
import_instance.import_data(data, associations, **opts)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def import_excel_klass
|
|
25
|
+
import_instance.tallty_excel
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative 'lib/tallty_import_export/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "tallty_import_export"
|
|
5
|
+
spec.version = TalltyImportExport::VERSION
|
|
6
|
+
spec.authors = ["liyijie"]
|
|
7
|
+
spec.email = ["liyijie825@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = %q{ import & export for active_record with simple_controller }
|
|
10
|
+
spec.description = %q{ import & export for active_record with simple_controller }
|
|
11
|
+
spec.homepage = "https://git.tallty.com/open-source/tallty_import_export"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
|
14
|
+
|
|
15
|
+
spec.platform = Gem::Platform::RUBY
|
|
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.
|
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
21
|
+
end
|
|
22
|
+
spec.bindir = "exe"
|
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.add_dependency('zip-zip')
|
|
27
|
+
spec.add_dependency('caxlsx')
|
|
28
|
+
spec.add_dependency('roo')
|
|
29
|
+
spec.add_dependency('roo-xls')
|
|
30
|
+
spec.add_dependency('tallty_form')
|
|
31
|
+
spec.add_dependency('activesupport')
|
|
32
|
+
spec.add_dependency('redis')
|
|
33
|
+
spec.add_dependency('redis-objects')
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tallty_import_export
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- liyijie
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-05-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: zip-zip
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: caxlsx
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: roo
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: roo-xls
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: tallty_form
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: activesupport
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: redis
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: redis-objects
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :runtime
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
description: " import & export for active_record with simple_controller "
|
|
126
|
+
email:
|
|
127
|
+
- liyijie825@gmail.com
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- ".gitignore"
|
|
133
|
+
- ".rspec"
|
|
134
|
+
- ".travis.yml"
|
|
135
|
+
- Gemfile
|
|
136
|
+
- Gemfile.lock
|
|
137
|
+
- LICENSE.txt
|
|
138
|
+
- README.md
|
|
139
|
+
- Rakefile
|
|
140
|
+
- bin/console
|
|
141
|
+
- bin/setup
|
|
142
|
+
- lib/tallty_import_export.rb
|
|
143
|
+
- lib/tallty_import_export/common.rb
|
|
144
|
+
- lib/tallty_import_export/context.rb
|
|
145
|
+
- lib/tallty_import_export/excel.rb
|
|
146
|
+
- lib/tallty_import_export/export.rb
|
|
147
|
+
- lib/tallty_import_export/exportable.rb
|
|
148
|
+
- lib/tallty_import_export/form_convert.rb
|
|
149
|
+
- lib/tallty_import_export/import.rb
|
|
150
|
+
- lib/tallty_import_export/importable.rb
|
|
151
|
+
- lib/tallty_import_export/version.rb
|
|
152
|
+
- tallty_import_export.gemspec
|
|
153
|
+
homepage: https://git.tallty.com/open-source/tallty_import_export
|
|
154
|
+
licenses:
|
|
155
|
+
- MIT
|
|
156
|
+
metadata: {}
|
|
157
|
+
post_install_message:
|
|
158
|
+
rdoc_options: []
|
|
159
|
+
require_paths:
|
|
160
|
+
- lib
|
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: 2.3.0
|
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
- - ">="
|
|
169
|
+
- !ruby/object:Gem::Version
|
|
170
|
+
version: '0'
|
|
171
|
+
requirements: []
|
|
172
|
+
rubygems_version: 3.2.15
|
|
173
|
+
signing_key:
|
|
174
|
+
specification_version: 4
|
|
175
|
+
summary: import & export for active_record with simple_controller
|
|
176
|
+
test_files: []
|