3 |
3 |
# Hipposoft 2008 #
|
4 |
4 |
# #
|
5 |
5 |
# History: 04-Jan-2008 (ADH): Created. #
|
6 |
|
# Feb 2009 (SJS): Hacked into plugin for redmine #
|
|
6 |
# Feb 2009 (SJS): Hacked into plugin for redmine #
|
|
7 |
# Apr 2010 (XEP): Fix some bugs #
|
7 |
8 |
########################################################################
|
8 |
9 |
|
9 |
10 |
class TaskImport
|
... | ... | |
25 |
26 |
require 'tempfile'
|
26 |
27 |
require 'rexml/document'
|
27 |
28 |
|
|
29 |
|
28 |
30 |
# Set up the import view. If there is no task data, this will consist of
|
29 |
31 |
# a file entry field and nothing else. If there is parsed file data (a
|
30 |
32 |
# preliminary task list), then this is included too.
|
... | ... | |
33 |
35 |
# This can and probably SHOULD be replaced with some URL rewrite magic
|
34 |
36 |
# now that the project loader is Redmine project based.
|
35 |
37 |
find_project()
|
|
38 |
|
36 |
39 |
end
|
37 |
40 |
|
38 |
41 |
# Take the task data from the 'new' view form and 'create' an "import
|
... | ... | |
43 |
46 |
def create
|
44 |
47 |
# This can and probably SHOULD be replaced with some URL rewrite magic
|
45 |
48 |
# now that the project loader is Redmine project based.
|
|
49 |
|
46 |
50 |
find_project()
|
47 |
51 |
|
48 |
52 |
# Set up a new TaskImport session object and read the XML file details
|
49 |
53 |
|
50 |
54 |
xmlfile = params[ :import ][ :xmlfile ]
|
51 |
55 |
@import = TaskImport.new
|
52 |
|
|
53 |
56 |
unless ( xmlfile.nil? )
|
54 |
|
|
55 |
57 |
# The user selected a file to upload, so process it
|
56 |
|
|
|
58 |
|
57 |
59 |
begin
|
58 |
60 |
|
59 |
61 |
# We assume XML files always begin with "<" in the first byte and
|
60 |
62 |
# if that's missing then it's GZip compressed. That's true in the
|
61 |
63 |
# limited case of project files.
|
62 |
|
|
|
64 |
|
|
65 |
|
63 |
66 |
byte = xmlfile.getc()
|
64 |
67 |
xmlfile.rewind()
|
|
68 |
|
65 |
69 |
|
66 |
70 |
xmlfile = Zlib::GzipReader.new( xmlfile ) if ( byte != '<'[ 0 ] )
|
67 |
71 |
xmldoc = REXML::Document.new( xmlfile.read() )
|
68 |
72 |
@import.tasks, @import.new_categories = get_tasks_from_xml( xmldoc )
|
69 |
73 |
|
|
74 |
|
|
75 |
|
70 |
76 |
if ( @import.tasks.nil? or @import.tasks.empty? )
|
|
77 |
|
71 |
78 |
flash[ :error ] = 'No usable tasks were found in that file'
|
72 |
79 |
else
|
73 |
80 |
flash[ :notice ] = 'Tasks read successfully. Please choose items to import.'
|
|
81 |
|
74 |
82 |
end
|
75 |
83 |
|
76 |
84 |
rescue => error
|
77 |
|
|
|
85 |
|
|
86 |
|
78 |
87 |
# REXML errors can be huge, including a full backtrace. It can cause
|
79 |
88 |
# session cookie overflow and we don't want the user to see it. Cut
|
80 |
89 |
# the message off at the first newline.
|
81 |
90 |
|
82 |
91 |
lines = error.message.split("\n")
|
83 |
92 |
flash[ :error ] = "Failed to read file: #{ lines[ 0 ] }"
|
|
93 |
logger.debug "DEBUG: ERROR -> #{ lines[ 0 ] }"
|
84 |
94 |
end
|
85 |
95 |
|
86 |
|
render( { :action => :new } )
|
|
96 |
render :action => :new
|
|
97 |
|
87 |
98 |
flash.delete( :error )
|
88 |
99 |
flash.delete( :notice )
|
89 |
100 |
|
... | ... | |
180 |
191 |
Issue.transaction do
|
181 |
192 |
to_import.each do | source_issue |
|
182 |
193 |
|
|
194 |
# We comment those lines because they are not necesary now.
|
183 |
195 |
# Add the category entry if necessary
|
184 |
|
category_entry = IssueCategory.find :first, :conditions => { :project_id => @project.id, :name => source_issue.category }
|
185 |
196 |
|
186 |
|
if (category_entry.nil?)
|
|
197 |
logger.debug "DEBUG: Issue to be imported: #{source_issue.inspect}"
|
|
198 |
if ( source_issue.category != "" )
|
|
199 |
logger.debug "DEBUG: Search category id by name: #{source_issue.category}"
|
|
200 |
category_entry = IssueCategory.find :first, :conditions => { :project_id => @project.id, :name => source_issue.category }
|
|
201 |
logger.debug "DEBUG: Category found: #{category_entry.inspect}"
|
|
202 |
else
|
|
203 |
category_entry = nil
|
|
204 |
end
|
|
205 |
|
|
206 |
#if (category_entry.nil?)
|
187 |
207 |
# Need to create it
|
188 |
|
category_entry = IssueCategory.new do |i|
|
189 |
|
i.name = source_issue.category
|
190 |
|
i.project_id = @project.id
|
191 |
|
end
|
192 |
208 |
|
193 |
|
category_entry.save!
|
194 |
|
end
|
|
209 |
# category_entry = IssueCategory.new do |i|
|
|
210 |
# i.name = source_issue.category
|
|
211 |
# i.project_id = @project.id
|
|
212 |
# end
|
|
213 |
|
|
214 |
# category_entry.save!
|
|
215 |
|
|
216 |
#end
|
195 |
217 |
|
196 |
218 |
destination_issue = Issue.new do |i|
|
197 |
219 |
i.tracker_id = default_tracker_id
|
198 |
|
i.category_id = category_entry.id
|
|
220 |
i.category_id = category_entry.id unless category_entry.nil?
|
199 |
221 |
i.subject = source_issue.title.slice(0, 255) # Max length of this field is 255
|
200 |
222 |
i.estimated_hours = source_issue.duration
|
201 |
223 |
i.project_id = @project.id
|
... | ... | |
206 |
228 |
i.start_date = source_issue.start
|
207 |
229 |
i.due_date = source_issue.finish unless source_issue.finish.nil?
|
208 |
230 |
i.due_date = (Date.parse(source_issue.start, false) + ((source_issue.duration.to_f/40.0)*7.0).to_i).to_s unless i.due_date != nil
|
209 |
|
|
|
231 |
logger.debug "DEBUG: Assigned_to field: #{source_issue.assigned_to}"
|
210 |
232 |
if ( source_issue.assigned_to != "" )
|
211 |
233 |
i.assigned_to_id = source_issue.assigned_to
|
212 |
|
i.status_id = IssueStatus.find_by_name("Assigned").id
|
|
234 |
i.status_id = IssueStatus.find_by_name("Asignada").id
|
213 |
235 |
end
|
214 |
236 |
end
|
215 |
237 |
|
216 |
238 |
destination_issue.save!
|
|
239 |
logger.debug "DEBUG: Issue #{destination_issue.description} imported"
|
|
240 |
|
217 |
241 |
|
218 |
242 |
# Now that we know this issue's Redmine issue ID, save it off for later
|
219 |
243 |
uidToIssueIdMap[ source_issue.uid ] = destination_issue.id
|
... | ... | |
238 |
262 |
end
|
239 |
263 |
end
|
240 |
264 |
end
|
241 |
|
|
242 |
|
redirect_to( "/projects/#{@project.identifier}/issues" )
|
|
265 |
|
|
266 |
redirect_to( "/projects/#{@project.identifier}/issues" )
|
243 |
267 |
|
244 |
268 |
|
245 |
269 |
rescue => error
|
246 |
270 |
flash[ :error ] = "Unable to import tasks: #{ error }"
|
|
271 |
logger.debug "DEBUG: Unable to import tasks: #{ error }"
|
247 |
272 |
render( { :action => :new } )
|
248 |
|
flash.delete( :error )
|
|
273 |
#flash.delete( :error )
|
249 |
274 |
|
250 |
275 |
end
|
251 |
276 |
end
|
... | ... | |
266 |
291 |
|
267 |
292 |
# Extract details of every task into a flat array
|
268 |
293 |
tasks = []
|
269 |
|
|
|
294 |
|
|
295 |
logger.debug "DEBUG: BEGIN get_tasks_from_xml"
|
|
296 |
|
270 |
297 |
doc.each_element( 'Project/Tasks/Task' ) do | task |
|
271 |
298 |
begin
|
|
299 |
|
|
300 |
logger.debug "Project/Tasks/Task found"
|
272 |
301 |
struct = OpenStruct.new
|
273 |
|
struct.level = task.get_elements( 'OutlineLevel' )[ 0 ].text.to_i
|
274 |
|
struct.tid = task.get_elements( 'ID' )[ 0 ].text.to_i
|
275 |
|
struct.uid = task.get_elements( 'UID' )[ 0 ].text.to_i
|
276 |
|
struct.title = task.get_elements( 'Name' )[ 0 ].text.strip
|
277 |
|
struct.start = task.get_elements( 'Start' )[ 0 ].text.split("T")[0]
|
|
302 |
struct.level = task.get_elements( 'OutlineLevel' )[ 0 ].text.to_i unless task.get_elements( 'OutlineLevel' )[ 0 ].nil?
|
|
303 |
struct.tid = task.get_elements( 'ID' )[ 0 ].text.to_i unless task.get_elements( 'ID' )[ 0 ].nil?
|
|
304 |
struct.uid = task.get_elements( 'UID' )[ 0 ].text.to_i unless task.get_elements( 'UID' )[ 0 ].nil?
|
|
305 |
struct.title = task.get_elements( 'Name' )[ 0 ].text.strip unless task.get_elements( 'Name' )[ 0 ].nil?
|
|
306 |
struct.start = task.get_elements( 'Start' )[ 0 ].text.split("T")[0] unless task.get_elements( 'Start' )[ 0 ].nil?
|
278 |
307 |
|
279 |
308 |
struct.finish = task.get_elements( 'Finish' )[ 0 ].text.split("T")[0] unless task.get_elements( 'Finish')[ 0 ].nil?
|
280 |
|
struct.percentcomplete = task.get_elements( 'PercentComplete')[0].text.to_i
|
281 |
|
|
|
309 |
# On Openproj xml files PercentComplete field could be not appear so we test if it exists.
|
|
310 |
logger.debug "DEBUG: Task Title: #{struct.title}"
|
|
311 |
if (task.get_elements( 'PercentComplete')[ 0 ].nil?)
|
|
312 |
duration = task.get_elements( 'Duration' )[ 0 ].text
|
|
313 |
remainingDuration = task.get_elements( 'RemainingDuration' )[ 0 ].text
|
|
314 |
logger.debug "DEBUG: Duration retrieved: #{duration}"
|
|
315 |
logger.debug "DEBUG: Remaining duration retrieved: #{remainingDuration}"
|
|
316 |
# Parse the "RemainingDuration" string: "PT<num>H<num>M<num>S", but with some
|
|
317 |
# leniency to allow any data before or after the H/M/S stuff.
|
|
318 |
hours = 0
|
|
319 |
mins = 0
|
|
320 |
secs = 0
|
|
321 |
strs = duration.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless duration.nil?
|
|
322 |
hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil?
|
|
323 |
duration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f
|
|
324 |
logger.debug "DEBUG: Task duration: #{duration}"
|
|
325 |
|
|
326 |
hours = 0
|
|
327 |
mins = 0
|
|
328 |
secs = 0
|
|
329 |
strs = remainingDuration.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless remainingDuration.nil?
|
|
330 |
hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil?
|
|
331 |
remainingDuration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f
|
|
332 |
logger.debug "DEBUG: Task Remaining Duration: #{remainingDuration}"
|
|
333 |
if ( duration > 0 )
|
|
334 |
percentcomplete = ( (duration - remainingDuration) * 100 ) / duration
|
|
335 |
else
|
|
336 |
percentcomplete = 0
|
|
337 |
end
|
|
338 |
logger.debug "DEBUG: PercentComplete: #{percentcomplete.to_i}"
|
|
339 |
struct.percentcomplete= percentcomplete.to_i
|
|
340 |
else
|
|
341 |
struct.percentcomplete=task.get_elements( 'PercentComplete')[ 0 ].text.to_i
|
|
342 |
logger.debug "DEBUG: PercentComplete retrieved: #{struct.percentcomplete}"
|
|
343 |
end
|
282 |
344 |
# Handle dependencies
|
283 |
345 |
struct.predecessors = []
|
284 |
|
task.each_element( 'PredecessorLink' ) do | predecessor |
|
285 |
|
begin
|
286 |
|
struct.predecessors.push( predecessor.get_elements('PredecessorUID')[0].text.to_i )
|
287 |
|
end
|
288 |
|
end
|
289 |
|
|
|
346 |
#task.each_element( 'PredecessorLink' ) do | predecessor |
|
|
347 |
# begin
|
|
348 |
# struct.predecessors.push( predecessor.get_elements('PredecessorUID')[0].text.to_i )
|
|
349 |
# end
|
|
350 |
#end
|
|
351 |
|
290 |
352 |
tasks.push( struct )
|
291 |
|
rescue
|
|
353 |
|
|
354 |
rescue => error
|
292 |
355 |
# Ignore errors; they tend to indicate malformed tasks, or at least,
|
293 |
356 |
# XML file task entries that we do not understand.
|
|
357 |
logger.debug "DEBUG: Unrecovered error getting tasks: #{error}"
|
294 |
358 |
end
|
295 |
359 |
end
|
296 |
|
|
|
360 |
|
|
361 |
|
297 |
362 |
# Sort the array by ID. By sorting the array this way, the order
|
298 |
363 |
# order will match the task order displayed to the user in the
|
299 |
364 |
# project editor software which generated the XML file.
|
300 |
|
|
|
365 |
|
301 |
366 |
tasks = tasks.sort_by { | task | task.tid }
|
302 |
367 |
|
303 |
368 |
# Step through the sorted tasks. Each time we find one where the
|
... | ... | |
314 |
379 |
next_task = tasks[ index + 1 ]
|
315 |
380 |
|
316 |
381 |
if ( next_task and next_task.level > task.level )
|
317 |
|
category = task.title.strip.gsub(/:$/, '') # Kill any trailing :'s which are common in some project files
|
318 |
|
all_categories.push(category) # Keep track of all categories so we know which ones might need to be added
|
|
382 |
# category = task.title.strip.gsub(/:$/, '') # Kill any trailing :'s which are common in some project files
|
|
383 |
# We dont want imported categories so we coment this line.
|
|
384 |
#all_categories.push(category) # Keep track of all categories so we know which ones might need to be added
|
319 |
385 |
tasks[ index ] = nil
|
320 |
386 |
else
|
321 |
387 |
task.category = category
|
... | ... | |
345 |
411 |
|
346 |
412 |
real_tasks = []
|
347 |
413 |
|
348 |
|
doc.each_element( 'Project/Assignments/Assignment' ) do | as |
|
349 |
|
task_uid = as.get_elements( 'TaskUID' )[ 0 ].text.to_i
|
350 |
|
task = uid_tasks[ task_uid ] unless task_uid.nil?
|
351 |
|
next if ( task.nil? )
|
|
414 |
#assig = doc.get_elements ( 'Project/Assignments/Assignment' )
|
|
415 |
#if ( !assig.nil? )
|
|
416 |
doc.each_element( 'Project/Assignments/Assignment' ) do | as |
|
|
417 |
task_uid = as.get_elements( 'TaskUID' )[ 0 ].text.to_i
|
|
418 |
task = uid_tasks[ task_uid ] unless task_uid.nil?
|
|
419 |
next if ( task.nil? )
|
352 |
420 |
|
353 |
|
work = as.get_elements( 'Work' )[ 0 ].text
|
|
421 |
work = as.get_elements( 'Work' )[ 0 ].text
|
354 |
422 |
|
355 |
|
# Parse the "Work" string: "PT<num>H<num>M<num>S", but with some
|
356 |
|
# leniency to allow any data before or after the H/M/S stuff.
|
357 |
|
hours = 0
|
358 |
|
mins = 0
|
359 |
|
secs = 0
|
|
423 |
# Parse the "Work" string: "PT<num>H<num>M<num>S", but with some
|
|
424 |
# leniency to allow any data before or after the H/M/S stuff.
|
|
425 |
hours = 0
|
|
426 |
mins = 0
|
|
427 |
secs = 0
|
360 |
428 |
|
361 |
|
strs = work.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless work.nil?
|
362 |
|
hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil?
|
|
429 |
strs = work.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless work.nil?
|
|
430 |
hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil?
|
363 |
431 |
|
364 |
|
#next if ( hours == 0 and mins == 0 and secs == 0 )
|
|
432 |
#next if ( hours == 0 and mins == 0 and secs == 0 )
|
365 |
433 |
|
366 |
|
# Woohoo, real task!
|
|
434 |
# Woohoo, real task!
|
367 |
435 |
|
368 |
|
task.duration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f
|
|
436 |
task.duration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f
|
369 |
437 |
|
370 |
|
real_tasks.push( task )
|
371 |
|
end
|
372 |
|
|
373 |
|
real_tasks = tasks if real_tasks.nil?
|
|
438 |
real_tasks.push( task )
|
|
439 |
end
|
|
440 |
#end
|
|
441 |
|
|
442 |
logger.debug "DEBUG: Real tasks: #{real_tasks.inspect}"
|
|
443 |
logger.debug "DEBUG: Tasks: #{tasks.inspect}"
|
|
444 |
|
|
445 |
real_tasks = tasks if real_tasks.empty?
|
374 |
446 |
real_tasks = real_tasks.uniq unless real_tasks.nil?
|
375 |
447 |
all_categories = all_categories.uniq.sort
|
376 |
448 |
|
|
449 |
logger.debug "DEBUG: END get_tasks_from_xml"
|
|
450 |
|
377 |
451 |
return real_tasks, all_categories
|
378 |
452 |
end
|
379 |
453 |
end
|