--- loader_controller.rb 2010-04-06 19:09:30.000000000 +0200 +++ loader_controller2.rb 2010-04-22 08:11:59.079745694 +0200 @@ -3,7 +3,8 @@ # Hipposoft 2008 # # # # History: 04-Jan-2008 (ADH): Created. # -# Feb 2009 (SJS): Hacked into plugin for redmine # +# Feb 2009 (SJS): Hacked into plugin for redmine # +# Apr 2010 (XEP): Fix some bugs # ######################################################################## class TaskImport @@ -25,6 +26,7 @@ require 'tempfile' require 'rexml/document' + # Set up the import view. If there is no task data, this will consist of # a file entry field and nothing else. If there is parsed file data (a # preliminary task list), then this is included too. @@ -33,6 +35,7 @@ # This can and probably SHOULD be replaced with some URL rewrite magic # now that the project loader is Redmine project based. find_project() + end # Take the task data from the 'new' view form and 'create' an "import @@ -43,47 +46,55 @@ def create # This can and probably SHOULD be replaced with some URL rewrite magic # now that the project loader is Redmine project based. + find_project() # Set up a new TaskImport session object and read the XML file details xmlfile = params[ :import ][ :xmlfile ] @import = TaskImport.new - unless ( xmlfile.nil? ) - # The user selected a file to upload, so process it - + begin # We assume XML files always begin with "<" in the first byte and # if that's missing then it's GZip compressed. That's true in the # limited case of project files. - + + byte = xmlfile.getc() xmlfile.rewind() + xmlfile = Zlib::GzipReader.new( xmlfile ) if ( byte != '<'[ 0 ] ) xmldoc = REXML::Document.new( xmlfile.read() ) @import.tasks, @import.new_categories = get_tasks_from_xml( xmldoc ) + + if ( @import.tasks.nil? or @import.tasks.empty? ) + flash[ :error ] = 'No usable tasks were found in that file' else flash[ :notice ] = 'Tasks read successfully. Please choose items to import.' + end rescue => error - + + # REXML errors can be huge, including a full backtrace. It can cause # session cookie overflow and we don't want the user to see it. Cut # the message off at the first newline. lines = error.message.split("\n") flash[ :error ] = "Failed to read file: #{ lines[ 0 ] }" + logger.debug "DEBUG: ERROR -> #{ lines[ 0 ] }" end - render( { :action => :new } ) + render :action => :new + flash.delete( :error ) flash.delete( :notice ) @@ -180,22 +191,33 @@ Issue.transaction do to_import.each do | source_issue | + # We comment those lines because they are not necesary now. # Add the category entry if necessary - category_entry = IssueCategory.find :first, :conditions => { :project_id => @project.id, :name => source_issue.category } - if (category_entry.nil?) + logger.debug "DEBUG: Issue to be imported: #{source_issue.inspect}" + if ( source_issue.category != "" ) + logger.debug "DEBUG: Search category id by name: #{source_issue.category}" + category_entry = IssueCategory.find :first, :conditions => { :project_id => @project.id, :name => source_issue.category } + logger.debug "DEBUG: Category found: #{category_entry.inspect}" + else + category_entry = nil + end + + #if (category_entry.nil?) # Need to create it - category_entry = IssueCategory.new do |i| - i.name = source_issue.category - i.project_id = @project.id - end - category_entry.save! - end + # category_entry = IssueCategory.new do |i| + # i.name = source_issue.category + # i.project_id = @project.id + # end + + # category_entry.save! + + #end destination_issue = Issue.new do |i| i.tracker_id = default_tracker_id - i.category_id = category_entry.id + i.category_id = category_entry.id unless category_entry.nil? i.subject = source_issue.title.slice(0, 255) # Max length of this field is 255 i.estimated_hours = source_issue.duration i.project_id = @project.id @@ -206,14 +228,16 @@ i.start_date = source_issue.start i.due_date = source_issue.finish unless source_issue.finish.nil? 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 - + logger.debug "DEBUG: Assigned_to field: #{source_issue.assigned_to}" if ( source_issue.assigned_to != "" ) i.assigned_to_id = source_issue.assigned_to - i.status_id = IssueStatus.find_by_name("Assigned").id + i.status_id = IssueStatus.find_by_name("Asignada").id end end destination_issue.save! + logger.debug "DEBUG: Issue #{destination_issue.description} imported" + # Now that we know this issue's Redmine issue ID, save it off for later uidToIssueIdMap[ source_issue.uid ] = destination_issue.id @@ -238,14 +262,15 @@ end end end - - redirect_to( "/projects/#{@project.identifier}/issues" ) + + redirect_to( "/projects/#{@project.identifier}/issues" ) rescue => error flash[ :error ] = "Unable to import tasks: #{ error }" + logger.debug "DEBUG: Unable to import tasks: #{ error }" render( { :action => :new } ) - flash.delete( :error ) + #flash.delete( :error ) end end @@ -266,38 +291,78 @@ # Extract details of every task into a flat array tasks = [] - + + logger.debug "DEBUG: BEGIN get_tasks_from_xml" + doc.each_element( 'Project/Tasks/Task' ) do | task | begin + + logger.debug "Project/Tasks/Task found" struct = OpenStruct.new - struct.level = task.get_elements( 'OutlineLevel' )[ 0 ].text.to_i - struct.tid = task.get_elements( 'ID' )[ 0 ].text.to_i - struct.uid = task.get_elements( 'UID' )[ 0 ].text.to_i - struct.title = task.get_elements( 'Name' )[ 0 ].text.strip - struct.start = task.get_elements( 'Start' )[ 0 ].text.split("T")[0] + struct.level = task.get_elements( 'OutlineLevel' )[ 0 ].text.to_i unless task.get_elements( 'OutlineLevel' )[ 0 ].nil? + struct.tid = task.get_elements( 'ID' )[ 0 ].text.to_i unless task.get_elements( 'ID' )[ 0 ].nil? + struct.uid = task.get_elements( 'UID' )[ 0 ].text.to_i unless task.get_elements( 'UID' )[ 0 ].nil? + struct.title = task.get_elements( 'Name' )[ 0 ].text.strip unless task.get_elements( 'Name' )[ 0 ].nil? + struct.start = task.get_elements( 'Start' )[ 0 ].text.split("T")[0] unless task.get_elements( 'Start' )[ 0 ].nil? struct.finish = task.get_elements( 'Finish' )[ 0 ].text.split("T")[0] unless task.get_elements( 'Finish')[ 0 ].nil? - struct.percentcomplete = task.get_elements( 'PercentComplete')[0].text.to_i - + # On Openproj xml files PercentComplete field could be not appear so we test if it exists. + logger.debug "DEBUG: Task Title: #{struct.title}" + if (task.get_elements( 'PercentComplete')[ 0 ].nil?) + duration = task.get_elements( 'Duration' )[ 0 ].text + remainingDuration = task.get_elements( 'RemainingDuration' )[ 0 ].text + logger.debug "DEBUG: Duration retrieved: #{duration}" + logger.debug "DEBUG: Remaining duration retrieved: #{remainingDuration}" + # Parse the "RemainingDuration" string: "PTHMS", but with some + # leniency to allow any data before or after the H/M/S stuff. + hours = 0 + mins = 0 + secs = 0 + strs = duration.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless duration.nil? + hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil? + duration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f + logger.debug "DEBUG: Task duration: #{duration}" + + hours = 0 + mins = 0 + secs = 0 + strs = remainingDuration.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless remainingDuration.nil? + hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil? + remainingDuration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f + logger.debug "DEBUG: Task Remaining Duration: #{remainingDuration}" + if ( duration > 0 ) + percentcomplete = ( (duration - remainingDuration) * 100 ) / duration + else + percentcomplete = 0 + end + logger.debug "DEBUG: PercentComplete: #{percentcomplete.to_i}" + struct.percentcomplete= percentcomplete.to_i + else + struct.percentcomplete=task.get_elements( 'PercentComplete')[ 0 ].text.to_i + logger.debug "DEBUG: PercentComplete retrieved: #{struct.percentcomplete}" + end # Handle dependencies struct.predecessors = [] - task.each_element( 'PredecessorLink' ) do | predecessor | - begin - struct.predecessors.push( predecessor.get_elements('PredecessorUID')[0].text.to_i ) - end - end - + #task.each_element( 'PredecessorLink' ) do | predecessor | + # begin + # struct.predecessors.push( predecessor.get_elements('PredecessorUID')[0].text.to_i ) + # end + #end + tasks.push( struct ) - rescue + + rescue => error # Ignore errors; they tend to indicate malformed tasks, or at least, # XML file task entries that we do not understand. + logger.debug "DEBUG: Unrecovered error getting tasks: #{error}" end end - + + # Sort the array by ID. By sorting the array this way, the order # order will match the task order displayed to the user in the # project editor software which generated the XML file. - + tasks = tasks.sort_by { | task | task.tid } # Step through the sorted tasks. Each time we find one where the @@ -314,8 +379,9 @@ next_task = tasks[ index + 1 ] if ( next_task and next_task.level > task.level ) - category = task.title.strip.gsub(/:$/, '') # Kill any trailing :'s which are common in some project files - all_categories.push(category) # Keep track of all categories so we know which ones might need to be added + # category = task.title.strip.gsub(/:$/, '') # Kill any trailing :'s which are common in some project files + # We dont want imported categories so we coment this line. + #all_categories.push(category) # Keep track of all categories so we know which ones might need to be added tasks[ index ] = nil else task.category = category @@ -345,35 +411,43 @@ real_tasks = [] - doc.each_element( 'Project/Assignments/Assignment' ) do | as | - task_uid = as.get_elements( 'TaskUID' )[ 0 ].text.to_i - task = uid_tasks[ task_uid ] unless task_uid.nil? - next if ( task.nil? ) + #assig = doc.get_elements ( 'Project/Assignments/Assignment' ) + #if ( !assig.nil? ) + doc.each_element( 'Project/Assignments/Assignment' ) do | as | + task_uid = as.get_elements( 'TaskUID' )[ 0 ].text.to_i + task = uid_tasks[ task_uid ] unless task_uid.nil? + next if ( task.nil? ) - work = as.get_elements( 'Work' )[ 0 ].text + work = as.get_elements( 'Work' )[ 0 ].text - # Parse the "Work" string: "PTHMS", but with some - # leniency to allow any data before or after the H/M/S stuff. - hours = 0 - mins = 0 - secs = 0 + # Parse the "Work" string: "PTHMS", but with some + # leniency to allow any data before or after the H/M/S stuff. + hours = 0 + mins = 0 + secs = 0 - strs = work.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless work.nil? - hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil? + strs = work.scan(/.*?(\d+)H(\d+)M(\d+)S.*?/).flatten unless work.nil? + hours, mins, secs = strs.map { | str | str.to_i } unless strs.nil? - #next if ( hours == 0 and mins == 0 and secs == 0 ) + #next if ( hours == 0 and mins == 0 and secs == 0 ) - # Woohoo, real task! + # Woohoo, real task! - task.duration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f + task.duration = ( ( ( hours * 3600 ) + ( mins * 60 ) + secs ) / 3600 ).prec_f - real_tasks.push( task ) - end - - real_tasks = tasks if real_tasks.nil? + real_tasks.push( task ) + end + #end + + logger.debug "DEBUG: Real tasks: #{real_tasks.inspect}" + logger.debug "DEBUG: Tasks: #{tasks.inspect}" + + real_tasks = tasks if real_tasks.empty? real_tasks = real_tasks.uniq unless real_tasks.nil? all_categories = all_categories.uniq.sort + logger.debug "DEBUG: END get_tasks_from_xml" + return real_tasks, all_categories end end