;]
[askyou.git] / lib / tasks / migrate_from_mantis.rake
blob594a7ec0e0e86f856af11663a9b903e45d950522
1 # redMine - project management software\r
2 # Copyright (C) 2006-2007  Jean-Philippe Lang\r
3 #\r
4 # This program is free software; you can redistribute it and/or\r
5 # modify it under the terms of the GNU General Public License\r
6 # as published by the Free Software Foundation; either version 2\r
7 # of the License, or (at your option) any later version.\r
8 \r
9 # This program is distributed in the hope that it will be useful,\r
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of\r
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
12 # GNU General Public License for more details.\r
13 \r
14 # You should have received a copy of the GNU General Public License\r
15 # along with this program; if not, write to the Free Software\r
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\r
18 desc 'Mantis migration script'\r
20 require 'active_record'\r
21 require 'iconv'\r
22 require 'pp'\r
24 namespace :redmine do\r
25 task :migrate_from_mantis => :environment do\r
26   \r
27   module MantisMigrate\r
28    \r
29       DEFAULT_STATUS = IssueStatus.default\r
30       assigned_status = IssueStatus.find_by_position(2)\r
31       resolved_status = IssueStatus.find_by_position(3)\r
32       feedback_status = IssueStatus.find_by_position(4)\r
33       closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }\r
34       STATUS_MAPPING = {10 => DEFAULT_STATUS,  # new\r
35                         20 => feedback_status, # feedback\r
36                         30 => DEFAULT_STATUS,  # acknowledged\r
37                         40 => DEFAULT_STATUS,  # confirmed\r
38                         50 => assigned_status, # assigned\r
39                         80 => resolved_status, # resolved\r
40                         90 => closed_status    # closed\r
41                         }\r
42                         \r
43       priorities = IssuePriority.all\r
44       DEFAULT_PRIORITY = priorities[2]\r
45       PRIORITY_MAPPING = {10 => priorities[1], # none\r
46                           20 => priorities[1], # low\r
47                           30 => priorities[2], # normal\r
48                           40 => priorities[3], # high\r
49                           50 => priorities[4], # urgent\r
50                           60 => priorities[5]  # immediate\r
51                           }\r
52     \r
53       TRACKER_BUG = Tracker.find_by_position(1)\r
54       TRACKER_FEATURE = Tracker.find_by_position(2)\r
55       \r
56       roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')\r
57       manager_role = roles[0]\r
58       developer_role = roles[1]\r
59       DEFAULT_ROLE = roles.last\r
60       ROLE_MAPPING = {10 => DEFAULT_ROLE,   # viewer\r
61                       25 => DEFAULT_ROLE,   # reporter\r
62                       40 => DEFAULT_ROLE,   # updater\r
63                       55 => developer_role, # developer\r
64                       70 => manager_role,   # manager\r
65                       90 => manager_role    # administrator\r
66                       }\r
67       \r
68       CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String\r
69                                    1 => 'int',    # Numeric\r
70                                    2 => 'int',    # Float\r
71                                    3 => 'list',   # Enumeration\r
72                                    4 => 'string', # Email\r
73                                    5 => 'bool',   # Checkbox\r
74                                    6 => 'list',   # List\r
75                                    7 => 'list',   # Multiselection list\r
76                                    8 => 'date',   # Date\r
77                                    }\r
78                                    \r
79       RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES,    # related to\r
80                                2 => IssueRelation::TYPE_RELATES,    # parent of\r
81                                3 => IssueRelation::TYPE_RELATES,    # child of\r
82                                0 => IssueRelation::TYPE_DUPLICATES, # duplicate of\r
83                                4 => IssueRelation::TYPE_DUPLICATES  # has duplicate\r
84                                }\r
85                                                                    \r
86     class MantisUser < ActiveRecord::Base\r
87       set_table_name :mantis_user_table\r
88       \r
89       def firstname\r
90         @firstname = realname.blank? ? username : realname.split.first[0..29]\r
91         @firstname.gsub!(/[^\w\s\'\-]/i, '')\r
92         @firstname\r
93       end\r
94       \r
95       def lastname\r
96         @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]\r
97         @lastname.gsub!(/[^\w\s\'\-]/i, '')\r
98         @lastname = '-' if @lastname.blank?\r
99         @lastname\r
100       end\r
101       \r
102       def email\r
103         if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&\r
104              !User.find_by_mail(read_attribute(:email))\r
105           @email = read_attribute(:email)\r
106         else\r
107           @email = "#{username}@foo.bar"\r
108         end\r
109       end\r
110       \r
111       def username\r
112         read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')\r
113       end\r
114     end\r
115     \r
116     class MantisProject < ActiveRecord::Base\r
117       set_table_name :mantis_project_table\r
118       has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id\r
119       has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id\r
120       has_many :news, :class_name => "MantisNews", :foreign_key => :project_id\r
121       has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id\r
122       \r
123       def name\r
124         read_attribute(:name)[0..29]\r
125       end\r
126       \r
127       def identifier\r
128         read_attribute(:name).underscore[0..19].gsub(/[^a-z0-9\-]/, '-')\r
129       end\r
130     end\r
131     \r
132     class MantisVersion < ActiveRecord::Base\r
133       set_table_name :mantis_project_version_table\r
134       \r
135       def version\r
136         read_attribute(:version)[0..29]\r
137       end\r
138       \r
139       def description\r
140         read_attribute(:description)[0..254]\r
141       end\r
142     end\r
143     \r
144     class MantisCategory < ActiveRecord::Base\r
145       set_table_name :mantis_project_category_table\r
146     end\r
147     \r
148     class MantisProjectUser < ActiveRecord::Base\r
149       set_table_name :mantis_project_user_list_table\r
150     end\r
151     \r
152     class MantisBug < ActiveRecord::Base\r
153       set_table_name :mantis_bug_table\r
154       belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id\r
155       has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id\r
156       has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id\r
157       has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id\r
158     end\r
159     \r
160     class MantisBugText < ActiveRecord::Base\r
161       set_table_name :mantis_bug_text_table\r
162       \r
163       # Adds Mantis steps_to_reproduce and additional_information fields\r
164       # to description if any\r
165       def full_description\r
166         full_description = description\r
167         full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?\r
168         full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?\r
169         full_description\r
170       end\r
171     end\r
172     \r
173     class MantisBugNote < ActiveRecord::Base\r
174       set_table_name :mantis_bugnote_table\r
175       belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id\r
176       belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id\r
177     end\r
178     \r
179     class MantisBugNoteText < ActiveRecord::Base\r
180       set_table_name :mantis_bugnote_text_table\r
181     end\r
182     \r
183     class MantisBugFile < ActiveRecord::Base\r
184       set_table_name :mantis_bug_file_table\r
185       \r
186       def size\r
187         filesize\r
188       end\r
189       \r
190       def original_filename\r
191         MantisMigrate.encode(filename)\r
192       end\r
193       \r
194       def content_type\r
195         file_type\r
196       end\r
197       \r
198       def read(*args)\r
199         if @read_finished\r
200                 nil\r
201         else\r
202                 @read_finished = true\r
203                 content\r
204         end\r
205       end\r
206     end\r
207     \r
208     class MantisBugRelationship < ActiveRecord::Base\r
209       set_table_name :mantis_bug_relationship_table\r
210     end\r
211     \r
212     class MantisBugMonitor < ActiveRecord::Base\r
213       set_table_name :mantis_bug_monitor_table\r
214     end\r
215     \r
216     class MantisNews < ActiveRecord::Base\r
217       set_table_name :mantis_news_table\r
218     end\r
219     \r
220     class MantisCustomField < ActiveRecord::Base\r
221       set_table_name :mantis_custom_field_table\r
222       set_inheritance_column :none  \r
223       has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id\r
224       has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id\r
225       \r
226       def format\r
227         read_attribute :type\r
228       end\r
229       \r
230       def name\r
231         read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')\r
232       end\r
233     end\r
234     \r
235     class MantisCustomFieldProject < ActiveRecord::Base\r
236       set_table_name :mantis_custom_field_project_table  \r
237     end\r
238     \r
239     class MantisCustomFieldString < ActiveRecord::Base\r
240       set_table_name :mantis_custom_field_string_table  \r
241     end\r
242   \r
243   \r
244     def self.migrate\r
245           \r
246       # Users\r
247       print "Migrating users"\r
248       User.delete_all "login <> 'admin'"\r
249       users_map = {}\r
250       users_migrated = 0\r
251       MantisUser.find(:all).each do |user|\r
252         u = User.new :firstname => encode(user.firstname), \r
253                                  :lastname => encode(user.lastname),\r
254                                  :mail => user.email,\r
255                                  :last_login_on => user.last_visit\r
256         u.login = user.username\r
257         u.password = 'mantis'\r
258         u.status = User::STATUS_LOCKED if user.enabled != 1\r
259         u.admin = true if user.access_level == 90\r
260         next unless u.save!\r
261         users_migrated += 1\r
262         users_map[user.id] = u.id\r
263         print '.'\r
264       end\r
265       puts\r
266     \r
267       # Projects\r
268       print "Migrating projects"\r
269       Project.destroy_all\r
270       projects_map = {}\r
271       versions_map = {}\r
272       categories_map = {}\r
273       MantisProject.find(:all).each do |project|\r
274         p = Project.new :name => encode(project.name), \r
275                         :description => encode(project.description)\r
276         p.identifier = project.identifier\r
277         next unless p.save\r
278         projects_map[project.id] = p.id\r
279         p.enabled_module_names = ['issue_tracking', 'news', 'wiki']\r
280         p.trackers << TRACKER_BUG\r
281         p.trackers << TRACKER_FEATURE\r
282         print '.'\r
283         \r
284         # Project members\r
285         project.members.each do |member|\r
286           m = Member.new :user => User.find_by_id(users_map[member.user_id]),\r
287                            :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]\r
288           m.project = p\r
289           m.save\r
290         end     \r
291         \r
292         # Project versions\r
293         project.versions.each do |version|\r
294           v = Version.new :name => encode(version.version),\r
295                           :description => encode(version.description),\r
296                           :effective_date => version.date_order.to_date\r
297           v.project = p\r
298           v.save\r
299           versions_map[version.id] = v.id\r
300         end\r
301         \r
302         # Project categories\r
303         project.categories.each do |category|\r
304           g = IssueCategory.new :name => category.category[0,30]\r
305           g.project = p\r
306           g.save\r
307           categories_map[category.category] = g.id\r
308         end\r
309       end       \r
310       puts      \r
311     \r
312       # Bugs\r
313       print "Migrating bugs"\r
314       Issue.destroy_all\r
315       issues_map = {}\r
316       keep_bug_ids = (Issue.count == 0)\r
317       MantisBug.find_each(:batch_size => 200) do |bug|\r
318         next unless projects_map[bug.project_id] && users_map[bug.reporter_id]\r
319         i = Issue.new :project_id => projects_map[bug.project_id], \r
320                       :subject => encode(bug.summary),\r
321                       :description => encode(bug.bug_text.full_description),\r
322                       :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,\r
323                       :created_on => bug.date_submitted,\r
324                       :updated_on => bug.last_updated\r
325         i.author = User.find_by_id(users_map[bug.reporter_id])\r
326         i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?\r
327         i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?\r
328         i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS\r
329         i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)\r
330         i.id = bug.id if keep_bug_ids\r
331         next unless i.save\r
332         issues_map[bug.id] = i.id\r
333         print '.'\r
334       STDOUT.flush\r
336         # Assignee\r
337         # Redmine checks that the assignee is a project member\r
338         if (bug.handler_id && users_map[bug.handler_id])\r
339           i.assigned_to = User.find_by_id(users_map[bug.handler_id])\r
340           i.save_with_validation(false)\r
341         end        \r
342         \r
343         # Bug notes\r
344         bug.bug_notes.each do |note|\r
345           next unless users_map[note.reporter_id]\r
346           n = Journal.new :notes => encode(note.bug_note_text.note),\r
347                           :created_on => note.date_submitted\r
348           n.user = User.find_by_id(users_map[note.reporter_id])\r
349           n.journalized = i\r
350           n.save\r
351         end\r
352         \r
353         # Bug files\r
354         bug.bug_files.each do |file|\r
355           a = Attachment.new :created_on => file.date_added\r
356           a.file = file\r
357           a.author = User.find :first\r
358           a.container = i\r
359           a.save\r
360         end\r
361         \r
362         # Bug monitors\r
363         bug.bug_monitors.each do |monitor|\r
364           next unless users_map[monitor.user_id]\r
365           i.add_watcher(User.find_by_id(users_map[monitor.user_id]))\r
366         end\r
367       end\r
368       \r
369       # update issue id sequence if needed (postgresql)\r
370       Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')\r
371       puts\r
372       \r
373       # Bug relationships\r
374       print "Migrating bug relations"\r
375       MantisBugRelationship.find(:all).each do |relation|\r
376         next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]\r
377         r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]\r
378         r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])\r
379         r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])\r
380         pp r unless r.save\r
381         print '.'\r
382         STDOUT.flush\r
383       end\r
384       puts\r
385       \r
386       # News\r
387       print "Migrating news"\r
388       News.destroy_all\r
389       MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|\r
390         next unless projects_map[news.project_id]\r
391         n = News.new :project_id => projects_map[news.project_id],\r
392                      :title => encode(news.headline[0..59]),\r
393                      :description => encode(news.body),\r
394                      :created_on => news.date_posted\r
395         n.author = User.find_by_id(users_map[news.poster_id])\r
396         n.save\r
397         print '.'\r
398         STDOUT.flush\r
399       end\r
400       puts\r
401       \r
402       # Custom fields\r
403       print "Migrating custom fields"\r
404       IssueCustomField.destroy_all\r
405       MantisCustomField.find(:all).each do |field|\r
406         f = IssueCustomField.new :name => field.name[0..29],\r
407                                  :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],\r
408                                  :min_length => field.length_min,\r
409                                  :max_length => field.length_max,\r
410                                  :regexp => field.valid_regexp,\r
411                                  :possible_values => field.possible_values.split('|'),\r
412                                  :is_required => field.require_report?\r
413         next unless f.save\r
414         print '.'\r
415         STDOUT.flush\r
416         # Trackers association\r
417         f.trackers = Tracker.find :all\r
418         \r
419         # Projects association\r
420         field.projects.each do |project|\r
421           f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]\r
422         end\r
423         \r
424         # Values\r
425         field.values.each do |value|\r
426           v = CustomValue.new :custom_field_id => f.id,\r
427                               :value => value.value\r
428           v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]\r
429           v.save\r
430         end unless f.new_record?\r
431       end\r
432       puts\r
433     \r
434       puts\r
435       puts "Users:           #{users_migrated}/#{MantisUser.count}"\r
436       puts "Projects:        #{Project.count}/#{MantisProject.count}"\r
437       puts "Memberships:     #{Member.count}/#{MantisProjectUser.count}"\r
438       puts "Versions:        #{Version.count}/#{MantisVersion.count}"\r
439       puts "Categories:      #{IssueCategory.count}/#{MantisCategory.count}"\r
440       puts "Bugs:            #{Issue.count}/#{MantisBug.count}"\r
441       puts "Bug notes:       #{Journal.count}/#{MantisBugNote.count}"\r
442       puts "Bug files:       #{Attachment.count}/#{MantisBugFile.count}"\r
443       puts "Bug relations:   #{IssueRelation.count}/#{MantisBugRelationship.count}"\r
444       puts "Bug monitors:    #{Watcher.count}/#{MantisBugMonitor.count}"\r
445       puts "News:            #{News.count}/#{MantisNews.count}"\r
446       puts "Custom fields:   #{IssueCustomField.count}/#{MantisCustomField.count}"\r
447     end\r
448   \r
449     def self.encoding(charset)\r
450       @ic = Iconv.new('UTF-8', charset)\r
451     rescue Iconv::InvalidEncoding\r
452       return false      \r
453     end\r
454     \r
455     def self.establish_connection(params)\r
456       constants.each do |const|\r
457         klass = const_get(const)\r
458         next unless klass.respond_to? 'establish_connection'\r
459         klass.establish_connection params\r
460       end\r
461     end\r
462     \r
463     def self.encode(text)\r
464       @ic.iconv text\r
465     rescue\r
466       text\r
467     end\r
468   end\r
469   \r
470   puts\r
471   if Redmine::DefaultData::Loader.no_data?\r
472     puts "Redmine configuration need to be loaded before importing data."\r
473     puts "Please, run this first:"\r
474     puts\r
475     puts "  rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""\r
476     exit\r
477   end\r
478   \r
479   puts "WARNING: Your Redmine data will be deleted during this process."\r
480   print "Are you sure you want to continue ? [y/N] "\r
481   STDOUT.flush\r
482   break unless STDIN.gets.match(/^y$/i)\r
483   \r
484   # Default Mantis database settings\r
485   db_params = {:adapter => 'mysql', \r
486                :database => 'bugtracker', \r
487                :host => 'localhost', \r
488                :username => 'root', \r
489                :password => '' }\r
491   puts                          \r
492   puts "Please enter settings for your Mantis database"  \r
493   [:adapter, :host, :database, :username, :password].each do |param|\r
494     print "#{param} [#{db_params[param]}]: "\r
495     value = STDIN.gets.chomp!\r
496     db_params[param] = value unless value.blank?\r
497   end\r
498     \r
499   while true\r
500     print "encoding [UTF-8]: "\r
501     STDOUT.flush\r
502     encoding = STDIN.gets.chomp!\r
503     encoding = 'UTF-8' if encoding.blank?\r
504     break if MantisMigrate.encoding encoding\r
505     puts "Invalid encoding!"\r
506   end\r
507   puts\r
508   \r
509   # Make sure bugs can refer bugs in other projects\r
510   Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'\r
511   \r
512   # Turn off email notifications\r
513   Setting.notified_events = []\r
514   \r
515   MantisMigrate.establish_connection db_params\r
516   MantisMigrate.migrate\r
517 end\r
518 end\r