1
|
# Redmine - project management software
|
2
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
3
|
#
|
4
|
# This program is free software; you can redistribute it and/or
|
5
|
# modify it under the terms of the GNU General Public License
|
6
|
# as published by the Free Software Foundation; either version 2
|
7
|
# of the License, or (at your option) any later version.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program; if not, write to the Free Software
|
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
|
18
|
#require 'iconv' if RUBY_VERSION < '1.9'
|
19
|
require 'pp'
|
20
|
|
21
|
desc <<-END_DESC
|
22
|
Import users, projects, and issues from CSV files.
|
23
|
|
24
|
Available options (provide at least one) :
|
25
|
* users => the path to a CSV file with one user per line
|
26
|
* projects => the path to a CSV file with one project per line
|
27
|
* issues => the path to a CSV file with one issue per line
|
28
|
|
29
|
Example:
|
30
|
rake redmine:import_from_csv users=/tmp/users.csv projects=/tmp/projects.csv issues=/tmp/issues.csv RAILS_ENV=production
|
31
|
END_DESC
|
32
|
|
33
|
namespace :redmine do
|
34
|
task :import_from_csv => :environment do
|
35
|
|
36
|
#go in rails root so that files paths can be relative
|
37
|
Dir.chdir(Rails.root)
|
38
|
|
39
|
class CSVImport
|
40
|
attr_accessor :options, :content, :mappings
|
41
|
|
42
|
def initialize(options)
|
43
|
@options = options
|
44
|
@content = {}
|
45
|
@mappings = {}
|
46
|
end
|
47
|
|
48
|
# Usage help
|
49
|
def self.usage
|
50
|
$stderr.puts 'Missing options! Try: rake -D redmine:import_from_csv to get some help.'
|
51
|
exit 1
|
52
|
end
|
53
|
|
54
|
# Global validation step
|
55
|
def validate
|
56
|
validate_params
|
57
|
parse_files
|
58
|
process_mappings
|
59
|
end
|
60
|
|
61
|
# Validate params and exits if user don't say "y"
|
62
|
def validate_params
|
63
|
puts
|
64
|
puts "You're about to import data in your '#{Rails.env}' instance."
|
65
|
puts "You'll use the following source files:"
|
66
|
puts " users: #{options['users'] || '-'} "
|
67
|
puts " projects: #{options['projects'] || '-'}"
|
68
|
puts " issues: #{options['issues'] || '-'}"
|
69
|
puts
|
70
|
puts "/!\\ Make sure to have a backup of your database before continuing."
|
71
|
puts
|
72
|
print 'Is this ok ? [y/n]: '
|
73
|
STDOUT.flush
|
74
|
ok = STDIN.gets.chomp!
|
75
|
exit 2 if ok != 'y'
|
76
|
puts
|
77
|
end
|
78
|
|
79
|
# Try to read each file and parse its CSV content
|
80
|
def parse_files
|
81
|
options.each do |type, filename|
|
82
|
begin
|
83
|
content[type] = FCSV.read(filename)
|
84
|
rescue CSV::MalformedCSVError
|
85
|
$stderr.puts "Error parsing #{filename}: #{$!.message}"
|
86
|
exit 1
|
87
|
rescue Errno::ENOENT, Errno::EACCES
|
88
|
$stderr.puts "Error reading #{filename}: #{$!.message}"
|
89
|
exit 1
|
90
|
end
|
91
|
end
|
92
|
end
|
93
|
|
94
|
# Validates if fields exist
|
95
|
def process_mappings
|
96
|
errors = 0
|
97
|
content.each do |type, lines|
|
98
|
fields = lines.shift
|
99
|
klass = type.classify.constantize
|
100
|
mappings[type] = []
|
101
|
fields.each do |field|
|
102
|
next if field == "project_identifier" && type == "issues"
|
103
|
if field.match(/^customfield(\d+)$/)
|
104
|
cf = CustomField.where(:type => "#{klass}CustomField", :id => $1).first
|
105
|
if cf.present?
|
106
|
mappings[type] << cf
|
107
|
else
|
108
|
$stderr.puts "Unable to find CustomField with type=#{klass}CustomField and id=#{$1}"
|
109
|
errors += 1
|
110
|
end
|
111
|
else
|
112
|
if klass.column_names.include?(field) || klass.instance_methods.include?(:"#{field}=")
|
113
|
mappings[type] << field
|
114
|
else
|
115
|
$stderr.puts "No field #{klass}##{field}"
|
116
|
errors += 1
|
117
|
end
|
118
|
end
|
119
|
end
|
120
|
end
|
121
|
exit 1 if errors > 0
|
122
|
end
|
123
|
|
124
|
# Runs the migration
|
125
|
def run
|
126
|
errors = []
|
127
|
puts
|
128
|
puts "Starting data import."
|
129
|
puts
|
130
|
%w(users projects issues).each do |type|
|
131
|
next unless content[type]
|
132
|
klass = type.classify.constantize
|
133
|
print "#{klass}: "
|
134
|
my_custom_fields = Array.new
|
135
|
content[type].each do |attributes|
|
136
|
object = klass.new
|
137
|
object.tracker = Tracker.first if klass == Issue
|
138
|
attributes.each_with_index do |value, index|
|
139
|
field = mappings[type][index]
|
140
|
if type == "issues" && field == "project_identifier"
|
141
|
object.project_id = Project.where("name = ? or name = ?", value, value).first
|
142
|
elsif field.is_a?(String)
|
143
|
object.send("#{field}=", value)
|
144
|
else
|
145
|
#customfield
|
146
|
v = CustomValue.new :custom_field_id => field.id,
|
147
|
:value => value
|
148
|
v.customized_type = "Issue"
|
149
|
my_custom_fields.push(v)
|
150
|
end
|
151
|
end
|
152
|
if object.valid?
|
153
|
print "."
|
154
|
object.save
|
155
|
#we have to create custom fields after creating issue because we need issue's ID
|
156
|
my_custom_fields.each do |mcf|
|
157
|
mcf.customized_id = object.id
|
158
|
#we have to replace existing default (zero) values with ones from CSV
|
159
|
cv = CustomValue.where(:customized_type => mcf.customized_type, :custom_field_id => mcf.custom_field_id, :customized_id => mcf.customized_id).first
|
160
|
if cv.present?
|
161
|
cv.value = mcf.value
|
162
|
cv.save
|
163
|
else
|
164
|
mcf.save
|
165
|
end
|
166
|
end
|
167
|
else
|
168
|
print "E"
|
169
|
errors << "Cannot save following line in #{type}: #{attributes.join(",")}\n => errors: #{object.errors.messages.inspect}\n => object: #{object.inspect}"
|
170
|
end
|
171
|
end
|
172
|
puts
|
173
|
end
|
174
|
puts
|
175
|
if errors.any?
|
176
|
puts "Errors:"
|
177
|
errors.each{|e| puts e}
|
178
|
end
|
179
|
end
|
180
|
end
|
181
|
|
182
|
# Extract options
|
183
|
options = {}
|
184
|
%w(users projects issues).each do |type|
|
185
|
options[type] = ENV[type].chomp if ENV[type]
|
186
|
end
|
187
|
|
188
|
# Exit if no valid params
|
189
|
CSVImport.usage if options.blank?
|
190
|
|
191
|
importer = CSVImport.new(options)
|
192
|
|
193
|
# Validate input
|
194
|
importer.validate
|
195
|
|
196
|
# Go!
|
197
|
old_notified_events = Setting.notified_events
|
198
|
begin
|
199
|
# Turn off email notifications temporarily
|
200
|
Setting.notified_events = []
|
201
|
# Run the migration
|
202
|
importer.run
|
203
|
ensure
|
204
|
# Restore previous notification settings even if the migration fails
|
205
|
Setting.notified_events = old_notified_events
|
206
|
end
|
207
|
end
|
208
|
end
|