kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
361 lines
12 KiB
361 lines
12 KiB
15 years ago
|
desc 'Import project into Redmine'
|
||
|
|
||
|
require 'yaml'
|
||
|
|
||
|
class Importer
|
||
|
def initialize(project, config, commit)
|
||
|
@yaml_project = project
|
||
|
@config = config
|
||
|
|
||
|
@commit = commit
|
||
|
|
||
|
@project = Project.find_by_name(project['name'])
|
||
|
raise "Project #{project['name']} does not exist" if @project.nil?
|
||
|
|
||
|
@tracker = {}
|
||
|
self.prime_trackers
|
||
|
@default_tracker = nil
|
||
|
|
||
|
@role = {}
|
||
|
@rolemap = config['roles'] || {}
|
||
|
@sprint = {}
|
||
|
@priority = {}
|
||
|
@time_entry_activity = config['activity'] || {}
|
||
|
@users = {}
|
||
|
end
|
||
|
|
||
|
def prime_trackers
|
||
|
@config['trackers'].each_pair {|name, data|
|
||
|
self.prime_tracker(name, data['tracker'], data['states'])
|
||
|
@default_tracker = name if data['default']
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def prime_tracker(issuetype, trackername, statusmap)
|
||
|
return if @tracker.include? issuetype and @tracker.include? trackername
|
||
|
|
||
|
if issuetype == '_task_':
|
||
|
tracker = Tracker.find_by_id(Task.tracker)
|
||
|
trackername = tracker.name
|
||
|
else
|
||
|
tracker = Tracker.find_by_name(trackername)
|
||
|
end
|
||
|
raise "Cannot find tracker #{issuetype}/#{trackername}, available trackers: #{@tracker.keys.join(', ')}" if tracker.nil?
|
||
|
|
||
|
statusmap ||= {}
|
||
|
|
||
|
foreign = {}
|
||
|
redmine = {}
|
||
|
tracker.workflows.each {|wf|
|
||
|
statusmap.each_pair {|f, r|
|
||
|
foreign[f] = wf.old_status if not foreign[f] and wf.old_status.name == r
|
||
|
foreign[f] = wf.new_status if not foreign[f] and wf.new_status.name == r
|
||
|
}
|
||
|
redmine[wf.old_status.name] = wf.old_status
|
||
|
redmine[wf.new_status.name] = wf.new_status
|
||
|
}
|
||
|
redmine.each_pair {|name, status|
|
||
|
foreign[name] = status if foreign[name]
|
||
|
}
|
||
|
|
||
|
@tracker[issuetype] = {'tracker' => tracker, 'status' => foreign}
|
||
|
end
|
||
|
|
||
|
def tracker(name, status = nil)
|
||
|
name = @default_tracker if not name
|
||
|
self.prime_tracker(name, name, nil) if not @tracker.include? name
|
||
|
|
||
|
t = @tracker[name]
|
||
|
raise "Tracker #{name} not found, available trackers: #{@tracker.keys.join(', ')}" if t.nil?
|
||
|
|
||
|
return t['tracker'] if status.nil?
|
||
|
|
||
|
s = t['status'][status]
|
||
|
raise "Status '#{status}' not found on tracker '#{name}', available: #{t['status'].keys.join(', ')}" if s.nil?
|
||
|
|
||
|
return t['status'][status]
|
||
|
end
|
||
|
|
||
|
def role(name)
|
||
|
role = @role[name] || Role.find_by_name(@rolemap[name]) || Role.find_by_name(name)
|
||
|
raise "Role '#{name}'/'#{@rolemap[name]}' not found, available: #{Role.find(:all).collect{|r| r.name}.join(', ')}" if role.nil?
|
||
|
|
||
|
@role[name] = role
|
||
|
|
||
|
return role
|
||
|
end
|
||
|
|
||
|
def self.newpass
|
||
|
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
||
|
pass = ""
|
||
|
1.upto(20) { |i| pass << chars[rand(chars.size-1)] }
|
||
|
return pass
|
||
|
end
|
||
|
|
||
|
def user(username)
|
||
|
u = @users[username] || User.find_by_login(username)
|
||
|
raise "Cannot find user '#{username}'" if not u
|
||
|
@users[username] = u
|
||
|
return u
|
||
|
end
|
||
|
|
||
|
def add_members
|
||
|
return if @yaml_project['members'].nil?
|
||
|
|
||
|
@yaml_project['members'].each {|username, data|
|
||
|
user = User.find_by_login(username)
|
||
|
if user.nil?
|
||
|
puts "New user #{username} (#{data['firstname']} #{data['lastname']}) will be created" if not @commit
|
||
|
user = User.new
|
||
|
user.login = username
|
||
|
user.password = Importer.newpass
|
||
|
user.firstname = data['firstname'].nil? ? username : data['firstname']
|
||
|
user.lastname = data['lastname'].nil? ? username : data['lastname']
|
||
|
user.mail = data['email'].nil? ? username + '@localhost.localdomain' : data['email']
|
||
|
user.admin = data['admin'].nil? ? false : data['admin']
|
||
|
user.status = data['active'] ? 1 : 3
|
||
|
user.type = "User"
|
||
|
user.save! if @commit
|
||
|
@users[username] = user if not @commit
|
||
|
end
|
||
|
|
||
|
membership = Member.find(:first, :conditions=> { :user_id => user.id, :project_id => @project.id })
|
||
|
if membership.nil?
|
||
|
membership = Member.new
|
||
|
membership.user = user
|
||
|
membership.project = @project
|
||
|
membership.roles << self.role(data['role'])
|
||
|
membership.save! if @commit
|
||
|
else
|
||
|
membership.roles << self.role(data['role'])
|
||
|
membership.save! if @commit
|
||
|
end
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def priority(p)
|
||
|
return IssuePriority.default if p.nil?
|
||
|
|
||
|
return @priority[p] if not @priority[p].nil?
|
||
|
|
||
|
prio = nil
|
||
|
if not @config['priority'].nil?
|
||
|
if not @config['priority'][p].nil?
|
||
|
prio = IssuePriority.find_by_name(@config['priority'][p])
|
||
|
@priority[p] = prio
|
||
|
@priority[@config['priority'][p]] = prio
|
||
|
end
|
||
|
else
|
||
|
prio = IssuePriority.find_by_name(p)
|
||
|
@priority[p] = prio
|
||
|
end
|
||
|
|
||
|
raise "No priority '#{p}'" if prio.nil?
|
||
|
return prio
|
||
|
end
|
||
|
|
||
|
def history(issue, j)
|
||
|
journal = Journal.new(:journalized => issue, :user => self.user(j['username']), :created_on => j['timestamp'])
|
||
|
|
||
|
activity = j['activity'] || {}
|
||
|
|
||
|
activity.each_pair {|prop, changes|
|
||
|
if prop == 'status'
|
||
|
journal.details << JournalDetail.new(
|
||
|
:property => 'attr',
|
||
|
:prop_key => 'status',
|
||
|
:old_value => self.tracker(issue.tracker.name, changes['old']),
|
||
|
:value => self.tracker(issue.tracker.name, changes['new']))
|
||
|
|
||
|
elsif prop == 'remaining_hours'
|
||
|
journal.details << JournalDetail.new(
|
||
|
:property => 'attr',
|
||
|
:prop_key => 'remaining_hours',
|
||
|
:old_value => changes['old'],
|
||
|
:value => changes['new'])
|
||
|
else
|
||
|
raise "Unhandled history '#{prop}'"
|
||
|
end
|
||
|
}
|
||
|
|
||
|
journal.save if @commit
|
||
|
end
|
||
|
|
||
|
def issues
|
||
|
@issues = {}
|
||
|
@yaml_project['issues'].each_pair {|id, i|
|
||
|
|
||
|
issue = Issue.new
|
||
|
|
||
|
i['history'].sort! {|a, b| a['timestamp'] <=> b['timestamp']} if i['history']
|
||
|
|
||
|
issue.subject = i['name']
|
||
|
issue.project = @project
|
||
|
issue.tracker = self.tracker(i['type'])
|
||
|
issue.status = self.tracker(i['type'], i['status'])
|
||
|
issue.author = self.user(i['author'])
|
||
|
issue.assigned_to = self.user(i['assigned_to']) if i['assigned_to']
|
||
|
issue.description = i['description']
|
||
|
issue.priority = self.priority(i['priority'])
|
||
|
issue.fixed_version = @sprints[i['sprint']]
|
||
|
issue.estimated_hours = i['estimated_hours']
|
||
|
issue.remaining_hours = i['remaining_hours']
|
||
|
|
||
|
issue.created_on = i['created']
|
||
|
if i['history']
|
||
|
issue.updated_on = i['history'][-1]['timestamp']
|
||
|
else
|
||
|
issue.updated_on = issue.created_on
|
||
|
end
|
||
|
|
||
|
issue.done_ratio = i['done'] || 0
|
||
|
|
||
|
issue.position = i['position']
|
||
|
issue.story_points = i['points']
|
||
|
|
||
|
issue.save! if @commit
|
||
|
@issues[id] = issue
|
||
|
|
||
|
if i['history']
|
||
|
i['history'].each{|j|
|
||
|
self.history(issue, j)
|
||
|
}
|
||
|
end
|
||
|
}
|
||
|
|
||
|
puts "Reparenting and blockers..."
|
||
|
@yaml_project['issues'].each_pair {|id, i|
|
||
|
if i['parent']
|
||
|
@issues[id].parent_issue_id = @issues[i['parent']].id
|
||
|
@issues[id].save! if @commit
|
||
|
end
|
||
|
|
||
|
if i['blocks']
|
||
|
i['blocks'].each { |blocked|
|
||
|
relation = IssueRelation.new :relation_type => IssueRelation::TYPE_BLOCKS
|
||
|
relation.issue_from = @issues[id]
|
||
|
relation.issue_to = @issues[blocked]
|
||
|
relation.save! if @commit
|
||
|
}
|
||
|
end
|
||
|
}
|
||
|
puts "Rebuilding issues tree..."
|
||
|
Issue.rebuild! if @commit
|
||
|
end
|
||
|
|
||
|
def sprints
|
||
|
return if @yaml_project['sprints'].nil?
|
||
|
|
||
|
@sprints = {}
|
||
|
@yaml_project['sprints'].each_pair {|id, s|
|
||
|
sprint = Version.new
|
||
|
sprint.project = @project
|
||
|
sprint.name = s['name']
|
||
|
sprint.description = s['description']
|
||
|
sprint.sprint_start_date = s['start']
|
||
|
sprint.effective_date = s['end']
|
||
|
|
||
|
# can't assign stories to closed sprint
|
||
|
sprint.status = 'open'
|
||
|
sprint.save! if @commit
|
||
|
@sprints[id] = sprint
|
||
|
|
||
|
if s['burndown']
|
||
|
s['burndown'].each {|bd|
|
||
|
bdd = BurndownDay.new
|
||
|
bdd.created_at = bd['date']
|
||
|
bdd.updated_at = bd['date']
|
||
|
bdd.points_committed = bd['points_committed'] || 0
|
||
|
bdd.points_accepted = bd['points_accepted'] || 0
|
||
|
bdd.points_resolved = bd['points_resolved'] || 0
|
||
|
bdd.remaining_hours = bd['remaining_hours'] || 0
|
||
|
bdd.version_id = sprint.id
|
||
|
bdd.save! if @commit
|
||
|
}
|
||
|
end
|
||
|
|
||
|
if s['stories']
|
||
|
s['stories'].each {|story|
|
||
|
self.story(story, sprint)
|
||
|
}
|
||
|
end
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def close_sprints
|
||
|
return if @yaml_project['sprints'].nil?
|
||
|
|
||
|
@yaml_project['sprints'].each_pair {|id, s|
|
||
|
@sprints[id].update_attribute(:status, 'closed') if not s['open'] and @commit
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def time_entry_activity(activity)
|
||
|
act = TimeEntryActivity.find_by_name(@time_entry_activity[activity] || activity || @time_entry_activity['default'])
|
||
|
raise "Unknown activity #{activity}, available: #{TimeEntryActivity.find(:all).collect{|a| a.name}.join(', ')}" if not act
|
||
|
return act
|
||
|
end
|
||
|
|
||
|
def time_entries
|
||
|
return if @yaml_project['time_entries'].nil?
|
||
|
|
||
|
@yaml_project['time_entries'].each {|te|
|
||
|
tl = TimeEntry.new(
|
||
|
:project => @project,
|
||
|
:issue => @issues[te['issue']],
|
||
|
:user => self.user(te['username']),
|
||
|
:spent_on => te['timestamp'],
|
||
|
:hours => te['hours'],
|
||
|
:comments => te['comments'],
|
||
|
:activity => self.time_entry_activity(te['activity']))
|
||
|
tl.save if @commit
|
||
|
}
|
||
|
end
|
||
|
|
||
|
def import!
|
||
|
puts "Importing '#{@project.name}'..."
|
||
|
if @config['clear'] and @commit
|
||
|
puts "Clearing time entries..."
|
||
|
TimeEntry.destroy_all({ :project_id => @project })
|
||
|
|
||
|
puts "Clearing issues..."
|
||
|
Issue.destroy_all({ :project_id => @project })
|
||
|
|
||
|
puts "Clearing burndown..."
|
||
|
Version.find(:all, :conditions => { :project_id => @project }).each {|sprint|
|
||
|
BurndownDay.destroy_all({ :version_id => sprint.id })
|
||
|
}
|
||
|
|
||
|
puts "Clearing sprints..."
|
||
|
Version.destroy_all({ :project_id => @project })
|
||
|
end
|
||
|
|
||
|
puts "Adding new users..."
|
||
|
self.add_members
|
||
|
|
||
|
puts "Adding sprints..."
|
||
|
self.sprints
|
||
|
|
||
|
puts "Adding issues..."
|
||
|
self.issues
|
||
|
|
||
|
puts "Closing sprints..."
|
||
|
self.close_sprints
|
||
|
|
||
|
puts "Time registration..."
|
||
|
self.time_entries
|
||
|
|
||
|
puts "Done!"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
namespace :redmine do
|
||
|
namespace :backlogs_plugin do
|
||
|
task :import_project => :environment do
|
||
|
raise "usage: rake redmine:backlogs_plugin:import_project project=<...> config=<...> mode=[commit|verify]" unless ENV['project'] and ENV['config']
|
||
|
importer = Importer.new(YAML::load_file(ENV['project']), YAML::load_file(ENV['config']), ENV['mode'] == 'commit')
|
||
|
importer.import!
|
||
|
end
|
||
|
end
|
||
|
end
|