Feature #22701 » 0003-Mulit-pass-CSV-import.patch
app/models/import.rb | ||
---|---|---|
139 | 139 |
end |
140 | 140 | |
141 | 141 |
# Imports items and returns the position of the last processed item |
142 |
def run(options={}) |
|
142 |
def run(options = {}, current_pass = completed_passes + 1) |
|
143 |
if current_pass > required_passes |
|
144 |
# Abort recursion |
|
145 | ||
146 |
update_attribute :finished, true |
|
147 |
remove_file |
|
148 | ||
149 |
return total_items |
|
150 |
end |
|
151 | ||
143 | 152 |
max_items = options[:max_items] |
144 | 153 |
max_time = options[:max_time] |
145 |
current = 0 |
|
146 | 154 |
imported = 0 |
147 |
resume_after = items.maximum(:position) || 0 |
|
148 | 155 |
interrupted = false |
149 | 156 |
started_on = Time.now |
150 | 157 | |
151 |
read_items do |row, position| |
|
152 |
if (max_items && imported >= max_items) || (max_time && Time.now >= started_on + max_time) |
|
158 |
current = run_pass(options, current_pass) do |position| |
|
159 |
if (max_items && imported >= max_items) || (max_time && Time.now - started_on >= max_time) |
|
160 |
# interrupt import |
|
153 | 161 |
interrupted = true |
154 |
break |
|
162 |
false |
|
163 |
else |
|
164 |
# continue importing item |
|
165 |
imported += 1 |
|
166 |
true |
|
155 | 167 |
end |
156 |
if position > resume_after |
|
157 |
item = items.build |
|
158 |
item.position = position |
|
159 | ||
160 |
if object = build_object(row) |
|
161 |
if object.save |
|
162 |
item.obj_id = object.id |
|
163 |
else |
|
164 |
item.message = object.errors.full_messages.join("\n") |
|
165 |
end |
|
166 |
end |
|
168 |
end |
|
167 | 169 | |
168 |
item.save! |
|
169 |
imported += 1 |
|
170 |
if !interrupted |
|
171 |
update_attribute(:total_items, current) if total_items.nil? |
|
172 | ||
173 |
if max_items |
|
174 |
options = options.merge(:max_items => max_items - imported) |
|
170 | 175 |
end |
171 |
current = position |
|
176 |
if max_time |
|
177 |
options = options.merge(:max_time => max_time - (Time.now - started_on)) |
|
178 |
end |
|
179 | ||
180 |
run(options, current_pass + 1) |
|
181 |
else |
|
182 |
current |
|
172 | 183 |
end |
184 |
end |
|
185 | ||
186 |
def run_pass(options={}, current_pass) |
|
187 |
resume_after = items.where(:completed_passes => current_pass).maximum(:position) || 0 |
|
173 | 188 | |
174 |
if imported == 0 || interrupted == false |
|
175 |
if total_items.nil? |
|
176 |
update_attribute :total_items, current |
|
189 |
current = 0 |
|
190 | ||
191 |
read_items do |row, position| |
|
192 |
next unless position > resume_after |
|
193 | ||
194 |
break unless yield(position) |
|
195 | ||
196 |
current = position |
|
197 | ||
198 |
item = items.where(:position => position).first_or_initialize |
|
199 | ||
200 |
if object = build_object(row, item, current_pass) |
|
201 |
if object.save |
|
202 |
item.obj_id = object.id |
|
203 |
else |
|
204 |
item.message = object.errors.full_messages.join("\n") |
|
205 |
end |
|
177 | 206 |
end |
178 |
update_attribute :finished, true |
|
179 |
remove_file |
|
207 | ||
208 |
item.completed_passes = current_pass |
|
209 |
item.save! |
|
180 | 210 |
end |
181 | 211 | |
182 | 212 |
current |
... | ... | |
190 | 220 |
items.where("obj_id IS NOT NULL") |
191 | 221 |
end |
192 | 222 | |
223 |
# Should be overridden in sub class to implement multi-pass import |
|
224 |
def required_passes |
|
225 |
1 |
|
226 |
end |
|
227 | ||
228 |
def completed_passes |
|
229 |
if total_items.present? && items.count == total_items |
|
230 |
items.minimum(:completed_passes) |
|
231 |
else |
|
232 |
0 |
|
233 |
end |
|
234 |
end |
|
235 | ||
193 | 236 |
private |
194 | 237 | |
195 | 238 |
def read_rows |
... | ... | |
223 | 266 | |
224 | 267 |
# Builds a record for the given row and returns it |
225 | 268 |
# To be implemented by subclasses |
226 |
def build_object(row) |
|
269 |
def build_object(row, item, pass) |
|
270 |
raise NotImplementedError, "Subclass responsibility" |
|
227 | 271 |
end |
228 | 272 | |
229 | 273 |
# Generates a filename used to store the import file |
app/models/import_item.rb | ||
---|---|---|
19 | 19 |
belongs_to :import |
20 | 20 | |
21 | 21 |
validates_presence_of :import_id, :position |
22 | ||
23 |
validates_numericality_of :completed_passes, :only_integer => true, |
|
24 |
:greater_than_or_equal_to => 0 |
|
22 | 25 |
end |
app/models/issue_import.rb | ||
---|---|---|
57 | 57 |
mapping['create_versions'] == '1' |
58 | 58 |
end |
59 | 59 | |
60 |
def required_passes |
|
61 |
if mapping['parent_issue_id'] |
|
62 |
2 |
|
63 |
else |
|
64 |
1 |
|
65 |
end |
|
66 |
end |
|
67 | ||
60 | 68 |
private |
61 | 69 | |
62 |
def build_object(row) |
|
63 |
issue = Issue.new |
|
64 |
issue.author = user |
|
70 |
def build_object(row, item, pass) |
|
71 |
if (pass == 1) |
|
72 |
issue = Issue.new |
|
73 |
issue.author = user |
|
74 |
build_issue(row, issue) |
|
75 |
else |
|
76 |
issue = Issue.find(item.obj_id) |
|
77 |
build_relations(row, issue) |
|
78 |
end |
|
65 | 79 |
issue.notify = false |
66 | 80 | |
81 |
issue |
|
82 |
end |
|
83 | ||
84 | ||
85 |
def build_issue(row, issue) |
|
67 | 86 |
attributes = { |
68 | 87 |
'project_id' => mapping['project_id'], |
69 | 88 |
'tracker_id' => mapping['tracker_id'], |
... | ... | |
110 | 129 |
attributes['is_private'] = '1' |
111 | 130 |
end |
112 | 131 |
end |
113 |
if parent_issue_id = row_value(row, 'parent_issue_id') |
|
114 |
if parent_issue_id =~ /\A(#)?(\d+)\z/ |
|
115 |
parent_issue_id = $2 |
|
116 |
if $1 |
|
117 |
attributes['parent_issue_id'] = parent_issue_id |
|
118 |
elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id) |
|
119 |
attributes['parent_issue_id'] = issue_id |
|
120 |
end |
|
121 |
else |
|
122 |
attributes['parent_issue_id'] = parent_issue_id |
|
123 |
end |
|
124 |
end |
|
125 | 132 |
if start_date = row_date(row, 'start_date') |
126 | 133 |
attributes['start_date'] = start_date |
127 | 134 |
end |
... | ... | |
151 | 158 |
issue.send :safe_attributes=, attributes, user |
152 | 159 |
issue |
153 | 160 |
end |
161 | ||
162 |
def build_relations(row, issue) |
|
163 |
attributes = {} |
|
164 | ||
165 |
if parent_issue_id = row_value(row, 'parent_issue_id') |
|
166 |
if parent_issue_id =~ /\A(#)?(\d+)\z/ |
|
167 |
parent_issue_id = $2 |
|
168 |
if $1 |
|
169 |
attributes['parent_issue_id'] = parent_issue_id |
|
170 |
elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id) |
|
171 |
attributes['parent_issue_id'] = issue_id |
|
172 |
end |
|
173 |
else |
|
174 |
attributes['parent_issue_id'] = parent_issue_id |
|
175 |
end |
|
176 |
end |
|
177 | ||
178 |
issue.send :safe_attributes=, attributes, user |
|
179 |
issue |
|
180 |
end |
|
154 | 181 |
end |
db/migrate/20160426125940_add_completed_passes_to_import_items.rb | ||
---|---|---|
1 |
class AddCompletedPassesToImportItems < ActiveRecord::Migration |
|
2 |
def self.up |
|
3 |
add_column :import_items, :completed_passes, :integer, :default => 0, :null => false |
|
4 |
end |
|
5 | ||
6 |
def self.donw |
|
7 |
remove_column :imports_item, :completed_passes |
|
8 |
end |
|
9 |
end |
test/unit/issue_import_test.rb | ||
---|---|---|
130 | 130 |
import.run |
131 | 131 |
assert !File.exists?(file_path) |
132 | 132 |
end |
133 | ||
134 |
def test_multi_step_run |
|
135 |
# max_items < total_items |
|
136 |
import = generate_import_with_mapping |
|
137 | ||
138 |
assert_difference 'Issue.count', 5 do |
|
139 |
assert_equal 2, import.run(:max_items => 2) |
|
140 |
assert_not import.finished |
|
141 |
assert_equal 4, import.run(:max_items => 2) |
|
142 |
assert_not import.finished |
|
143 |
assert_equal 5, import.run(:max_items => 2) |
|
144 |
assert import.finished |
|
145 |
end |
|
146 | ||
147 | ||
148 |
# max_items > total_items |
|
149 |
import = generate_import_with_mapping |
|
150 | ||
151 |
assert_difference 'Issue.count', 5 do |
|
152 |
assert_equal 5, import.run(:max_items => 6) |
|
153 |
assert import.finished |
|
154 |
end |
|
155 | ||
156 | ||
157 |
# max_items == total_items |
|
158 |
import = generate_import_with_mapping |
|
159 | ||
160 |
assert_difference 'Issue.count', 5 do |
|
161 |
assert_equal 5, import.run(:max_items => 5) |
|
162 |
assert import.finished |
|
163 |
end |
|
164 |
end |
|
165 | ||
166 |
def test_multi_step_multi_pass_run |
|
167 |
# max_items < total_items |
|
168 |
import = generate_import_with_mapping |
|
169 |
import.mapping.merge!('parent_issue_id' => '5') |
|
170 | ||
171 |
assert_difference 'Issue.count', 5 do |
|
172 |
assert_equal 2, import.run(:max_items => 2) |
|
173 |
assert_not import.finished |
|
174 | ||
175 |
assert_equal 4, import.run(:max_items => 2) |
|
176 |
assert_not import.finished |
|
177 | ||
178 |
assert_equal 1, import.run(:max_items => 2) |
|
179 |
assert_not import.finished |
|
180 | ||
181 |
assert_equal 3, import.run(:max_items => 2) |
|
182 |
assert_not import.finished |
|
183 | ||
184 |
assert_equal 5, import.run(:max_items => 2) |
|
185 |
assert import.finished |
|
186 |
end |
|
187 | ||
188 | ||
189 |
# max_items > total_items |
|
190 |
import = generate_import_with_mapping |
|
191 |
import.mapping.merge!('parent_issue_id' => '5') |
|
192 | ||
193 |
assert_difference 'Issue.count', 5 do |
|
194 |
assert_equal 1, import.run(:max_items => 6) |
|
195 |
assert_not import.finished |
|
196 | ||
197 |
assert_equal 5, import.run(:max_items => 6) |
|
198 |
assert import.finished |
|
199 |
end |
|
200 | ||
201 | ||
202 |
# max_items == total_items |
|
203 |
import = generate_import_with_mapping |
|
204 |
import.mapping.merge!('parent_issue_id' => '5') |
|
205 | ||
206 |
assert_difference 'Issue.count', 5 do |
|
207 |
assert_equal 0, import.run(:max_items => 5) |
|
208 |
assert_not import.finished |
|
209 | ||
210 |
assert_equal 5, import.run(:max_items => 5) |
|
211 |
assert import.finished |
|
212 |
end |
|
213 |
end |
|
214 | ||
215 |
def test_required_passes |
|
216 |
# Imports w/o relation mappings need just a single pass |
|
217 |
import = generate_import_with_mapping |
|
218 | ||
219 |
assert_equal 1, import.required_passes |
|
220 |
import.run |
|
221 |
assert_equal 1, import.completed_passes |
|
222 | ||
223 |
# Imports w/ references to other rows need 2 passes |
|
224 |
import = generate_import_with_mapping |
|
225 |
import.mapping.merge!('parent_issue_id' => '5') |
|
226 | ||
227 |
assert_equal 2, import.required_passes |
|
228 |
import.run |
|
229 |
assert_equal 2, import.completed_passes |
|
230 |
end |
|
133 | 231 |
end |
- « Previous
- 1
- 2
- 3
- Next »