1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
# coding: utf-8
class SimpleExporter < SimpleInterface
def export opts={}
configuration.validate!
init_env opts
@csv = nil
fail_with_error "Unable to write in file: #{self.filepath}" do
dir = Pathname.new(self.filepath).dirname
FileUtils.mkdir_p dir
@csv = CSV.open(self.filepath, 'w', self.configuration.csv_options)
end
self.configuration.before_actions(:parsing).each do |action| action.call self end
@statuses = ""
process_collection
self.status ||= :success
rescue SimpleInterface::FailedOperation
self.status = :failed
ensure
@csv&.close
task_finished
end
def collection
@collection ||= begin
coll = configuration.collection
coll = coll.call() if coll.is_a?(Proc)
coll
end
end
def encode_string s
s.encode("utf-8").force_encoding("utf-8")
end
protected
def init_env opts
@number_of_lines = collection.size
super opts
end
def process_collection
self.configuration.before_actions(:all).each do |action| action.call self end
log "Starting export ..."
log "Export will be written in #{filepath}"
@csv << self.configuration.columns.map(&:name)
if collection.is_a?(ActiveRecord::Relation) && collection.model.column_names.include?("id")
ids = collection.pluck :id
ids.in_groups_of(configuration.batch_size).each do |batch_ids|
collection.where(id: batch_ids).each do |item|
handle_item item
end
end
else
collection.each{|item| handle_item item }
end
print_state
end
def map_item_to_rows item
return [item] unless configuration.item_to_rows_mapping
configuration.item_to_rows_mapping.call(item).map {|row| row.is_a?(ActiveRecord::Base) ? row : CustomRow.new(row) }
end
def resolve_value item, col
scoped_item = col.scope.inject(item){|tmp, scope| tmp.send(scope)}
val = col[:value]
if val.nil? || val.is_a?(Proc)
if val.is_a?(Proc)
val = instance_exec(scoped_item, &val)
else
attributes = [col.attribute].flatten
val = attributes.inject(scoped_item){|tmp, attr| tmp.send(attr) if tmp }
end
end
if val.nil? && !col.omit_nil?
push_in_journal({event: :attribute_not_found, message: "Value missing for: #{[col.scope, col.attribute].flatten.join('.')}", kind: :warning})
self.status ||= :success_with_warnings
@new_status ||= colorize("✓", :orange)
end
if val.nil? && col.required?
@new_status = colorize("x", :red)
raise "MISSING VALUE FOR COLUMN #{col.name}"
end
val = encode_string(val) if val.is_a?(String)
val
end
def handle_item item
number_of_lines = @number_of_lines
@current_item = item
map_item_to_rows(item).each_with_index do |item, i|
@number_of_lines = number_of_lines + i
@current_row = item.attributes
@current_row = @current_row.slice(*configuration.logged_attributes) if configuration.logged_attributes.present?
row = []
@new_status = nil
self.configuration.columns.each do |col|
val = resolve_value item, col
@new_status ||= colorize("✓", :green)
row << val
end
push_in_journal({event: :success, kind: :log})
@statuses += @new_status
print_state if @current_line % 20 == 0 || i > 0
@current_line += 1
@csv << row
end
end
class CustomRow < OpenStruct
def initialize data
super data
@data = data
end
def attributes
flatten_hash @data
end
protected
def flatten_hash h
h.each_with_object({}) do |(k, v), h|
if v.is_a? Hash
flatten_hash(v).map do |h_k, h_v|
h["#{k}.#{h_k}".to_sym] = h_v
end
elsif v.is_a? ActiveRecord::Base
flatten_hash(v.attributes).map do |h_k, h_v|
h["#{k}.#{h_k}".to_sym] = h_v
end
else
h[k] = v
end
end
end
end
class Configuration < SimpleInterface::Configuration
attr_accessor :collection
attr_accessor :batch_size
attr_accessor :logged_attributes
attr_accessor :item_to_rows_mapping
def initialize import_name, opts={}
super import_name, opts
@collection = opts[:collection]
@batch_size = opts[:batch_size] || 1000
@logged_attributes = opts[:logged_attributes]
@item_to_rows_mapping = opts[:item_to_rows_mapping]
end
def options
super.update({
collection: collection,
batch_size: batch_size,
logged_attributes: logged_attributes,
item_to_rows_mapping: item_to_rows_mapping,
})
end
def map_item_to_rows &block
@item_to_rows_mapping = block
end
def add_column name, opts={}
raise "Column already defined: #{name}" if @columns.any?{|c| c.name == name.to_s}
super name, opts
end
def validate!
raise "Incomplete configuration, missing collection for #{@import_name}" if collection.nil?
end
end
end
|