##// END OF EJS Templates
Remove the limitation on characters that can be used in custom_field, issue_status, role, tracker, user names (#5152)....
Jean-Philippe Lang -
r4479:44ffc5a3365f
parent child
Show More
@@ -1,121 +1,120
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 has_many :custom_values, :dependent => :delete_all
19 has_many :custom_values, :dependent => :delete_all
20 acts_as_list :scope => 'type = \'#{self.class}\''
20 acts_as_list :scope => 'type = \'#{self.class}\''
21 serialize :possible_values
21 serialize :possible_values
22
22
23 validates_presence_of :name, :field_format
23 validates_presence_of :name, :field_format
24 validates_uniqueness_of :name, :scope => :type
24 validates_uniqueness_of :name, :scope => :type
25 validates_length_of :name, :maximum => 30
25 validates_length_of :name, :maximum => 30
26 validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
27 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
26 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
28
27
29 def initialize(attributes = nil)
28 def initialize(attributes = nil)
30 super
29 super
31 self.possible_values ||= []
30 self.possible_values ||= []
32 end
31 end
33
32
34 def before_validation
33 def before_validation
35 # make sure these fields are not searchable
34 # make sure these fields are not searchable
36 self.searchable = false if %w(int float date bool).include?(field_format)
35 self.searchable = false if %w(int float date bool).include?(field_format)
37 true
36 true
38 end
37 end
39
38
40 def validate
39 def validate
41 if self.field_format == "list"
40 if self.field_format == "list"
42 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
41 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
43 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
42 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
44 end
43 end
45
44
46 # validate default value
45 # validate default value
47 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
46 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
48 v.custom_field.is_required = false
47 v.custom_field.is_required = false
49 errors.add(:default_value, :invalid) unless v.valid?
48 errors.add(:default_value, :invalid) unless v.valid?
50 end
49 end
51
50
52 # Makes possible_values accept a multiline string
51 # Makes possible_values accept a multiline string
53 def possible_values=(arg)
52 def possible_values=(arg)
54 if arg.is_a?(Array)
53 if arg.is_a?(Array)
55 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
54 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
56 else
55 else
57 self.possible_values = arg.to_s.split(/[\n\r]+/)
56 self.possible_values = arg.to_s.split(/[\n\r]+/)
58 end
57 end
59 end
58 end
60
59
61 def cast_value(value)
60 def cast_value(value)
62 casted = nil
61 casted = nil
63 unless value.blank?
62 unless value.blank?
64 case field_format
63 case field_format
65 when 'string', 'text', 'list'
64 when 'string', 'text', 'list'
66 casted = value
65 casted = value
67 when 'date'
66 when 'date'
68 casted = begin; value.to_date; rescue; nil end
67 casted = begin; value.to_date; rescue; nil end
69 when 'bool'
68 when 'bool'
70 casted = (value == '1' ? true : false)
69 casted = (value == '1' ? true : false)
71 when 'int'
70 when 'int'
72 casted = value.to_i
71 casted = value.to_i
73 when 'float'
72 when 'float'
74 casted = value.to_f
73 casted = value.to_f
75 end
74 end
76 end
75 end
77 casted
76 casted
78 end
77 end
79
78
80 # Returns a ORDER BY clause that can used to sort customized
79 # Returns a ORDER BY clause that can used to sort customized
81 # objects by their value of the custom field.
80 # objects by their value of the custom field.
82 # Returns false, if the custom field can not be used for sorting.
81 # Returns false, if the custom field can not be used for sorting.
83 def order_statement
82 def order_statement
84 case field_format
83 case field_format
85 when 'string', 'text', 'list', 'date', 'bool'
84 when 'string', 'text', 'list', 'date', 'bool'
86 # COALESCE is here to make sure that blank and NULL values are sorted equally
85 # COALESCE is here to make sure that blank and NULL values are sorted equally
87 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
86 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
88 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
87 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
89 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
88 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
90 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
89 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
91 when 'int', 'float'
90 when 'int', 'float'
92 # Make the database cast values into numeric
91 # Make the database cast values into numeric
93 # Postgresql will raise an error if a value can not be casted!
92 # Postgresql will raise an error if a value can not be casted!
94 # CustomValue validations should ensure that it doesn't occur
93 # CustomValue validations should ensure that it doesn't occur
95 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
94 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
96 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
95 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
97 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
96 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
98 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
97 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
99 else
98 else
100 nil
99 nil
101 end
100 end
102 end
101 end
103
102
104 def <=>(field)
103 def <=>(field)
105 position <=> field.position
104 position <=> field.position
106 end
105 end
107
106
108 def self.customized_class
107 def self.customized_class
109 self.name =~ /^(.+)CustomField$/
108 self.name =~ /^(.+)CustomField$/
110 begin; $1.constantize; rescue nil; end
109 begin; $1.constantize; rescue nil; end
111 end
110 end
112
111
113 # to move in project_custom_field
112 # to move in project_custom_field
114 def self.for_all
113 def self.for_all
115 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
114 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
116 end
115 end
117
116
118 def type_name
117 def type_name
119 nil
118 nil
120 end
119 end
121 end
120 end
@@ -1,99 +1,98
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueStatus < ActiveRecord::Base
18 class IssueStatus < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :workflows, :foreign_key => "old_status_id"
20 has_many :workflows, :foreign_key => "old_status_id"
21 acts_as_list
21 acts_as_list
22
22
23 before_destroy :delete_workflows
23 before_destroy :delete_workflows
24
24
25 validates_presence_of :name
25 validates_presence_of :name
26 validates_uniqueness_of :name
26 validates_uniqueness_of :name
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
29 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
28 validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
30
29
31 def after_save
30 def after_save
32 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
31 IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
33 end
32 end
34
33
35 # Returns the default status for new issues
34 # Returns the default status for new issues
36 def self.default
35 def self.default
37 find(:first, :conditions =>["is_default=?", true])
36 find(:first, :conditions =>["is_default=?", true])
38 end
37 end
39
38
40 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
39 # Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
41 def self.update_issue_done_ratios
40 def self.update_issue_done_ratios
42 if Issue.use_status_for_done_ratio?
41 if Issue.use_status_for_done_ratio?
43 IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
42 IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
44 Issue.update_all(["done_ratio = ?", status.default_done_ratio],
43 Issue.update_all(["done_ratio = ?", status.default_done_ratio],
45 ["status_id = ?", status.id])
44 ["status_id = ?", status.id])
46 end
45 end
47 end
46 end
48
47
49 return Issue.use_status_for_done_ratio?
48 return Issue.use_status_for_done_ratio?
50 end
49 end
51
50
52 # Returns an array of all statuses the given role can switch to
51 # Returns an array of all statuses the given role can switch to
53 # Uses association cache when called more than one time
52 # Uses association cache when called more than one time
54 def new_statuses_allowed_to(roles, tracker)
53 def new_statuses_allowed_to(roles, tracker)
55 if roles && tracker
54 if roles && tracker
56 role_ids = roles.collect(&:id)
55 role_ids = roles.collect(&:id)
57 new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
56 new_statuses = workflows.select {|w| role_ids.include?(w.role_id) && w.tracker_id == tracker.id}.collect{|w| w.new_status}.compact.sort
58 else
57 else
59 []
58 []
60 end
59 end
61 end
60 end
62
61
63 # Same thing as above but uses a database query
62 # Same thing as above but uses a database query
64 # More efficient than the previous method if called just once
63 # More efficient than the previous method if called just once
65 def find_new_statuses_allowed_to(roles, tracker)
64 def find_new_statuses_allowed_to(roles, tracker)
66 if roles && tracker
65 if roles && tracker
67 workflows.find(:all,
66 workflows.find(:all,
68 :include => :new_status,
67 :include => :new_status,
69 :conditions => { :role_id => roles.collect(&:id),
68 :conditions => { :role_id => roles.collect(&:id),
70 :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
69 :tracker_id => tracker.id}).collect{ |w| w.new_status }.compact.sort
71 else
70 else
72 []
71 []
73 end
72 end
74 end
73 end
75
74
76 def new_status_allowed_to?(status, roles, tracker)
75 def new_status_allowed_to?(status, roles, tracker)
77 if status && roles && tracker
76 if status && roles && tracker
78 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
77 !workflows.find(:first, :conditions => {:new_status_id => status.id, :role_id => roles.collect(&:id), :tracker_id => tracker.id}).nil?
79 else
78 else
80 false
79 false
81 end
80 end
82 end
81 end
83
82
84 def <=>(status)
83 def <=>(status)
85 position <=> status.position
84 position <=> status.position
86 end
85 end
87
86
88 def to_s; name end
87 def to_s; name end
89
88
90 private
89 private
91 def check_integrity
90 def check_integrity
92 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
91 raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
93 end
92 end
94
93
95 # Deletes associated workflows
94 # Deletes associated workflows
96 def delete_workflows
95 def delete_workflows
97 Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
96 Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
98 end
97 end
99 end
98 end
@@ -1,163 +1,162
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Role < ActiveRecord::Base
18 class Role < ActiveRecord::Base
19 # Built-in roles
19 # Built-in roles
20 BUILTIN_NON_MEMBER = 1
20 BUILTIN_NON_MEMBER = 1
21 BUILTIN_ANONYMOUS = 2
21 BUILTIN_ANONYMOUS = 2
22
22
23 named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
23 named_scope :givable, { :conditions => "builtin = 0", :order => 'position' }
24 named_scope :builtin, lambda { |*args|
24 named_scope :builtin, lambda { |*args|
25 compare = 'not' if args.first == true
25 compare = 'not' if args.first == true
26 { :conditions => "#{compare} builtin = 0" }
26 { :conditions => "#{compare} builtin = 0" }
27 }
27 }
28
28
29 before_destroy :check_deletable
29 before_destroy :check_deletable
30 has_many :workflows, :dependent => :delete_all do
30 has_many :workflows, :dependent => :delete_all do
31 def copy(source_role)
31 def copy(source_role)
32 Workflow.copy(nil, source_role, nil, proxy_owner)
32 Workflow.copy(nil, source_role, nil, proxy_owner)
33 end
33 end
34 end
34 end
35
35
36 has_many :member_roles, :dependent => :destroy
36 has_many :member_roles, :dependent => :destroy
37 has_many :members, :through => :member_roles
37 has_many :members, :through => :member_roles
38 acts_as_list
38 acts_as_list
39
39
40 serialize :permissions, Array
40 serialize :permissions, Array
41 attr_protected :builtin
41 attr_protected :builtin
42
42
43 validates_presence_of :name
43 validates_presence_of :name
44 validates_uniqueness_of :name
44 validates_uniqueness_of :name
45 validates_length_of :name, :maximum => 30
45 validates_length_of :name, :maximum => 30
46 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
47
46
48 def permissions
47 def permissions
49 read_attribute(:permissions) || []
48 read_attribute(:permissions) || []
50 end
49 end
51
50
52 def permissions=(perms)
51 def permissions=(perms)
53 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
52 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
54 write_attribute(:permissions, perms)
53 write_attribute(:permissions, perms)
55 end
54 end
56
55
57 def add_permission!(*perms)
56 def add_permission!(*perms)
58 self.permissions = [] unless permissions.is_a?(Array)
57 self.permissions = [] unless permissions.is_a?(Array)
59
58
60 permissions_will_change!
59 permissions_will_change!
61 perms.each do |p|
60 perms.each do |p|
62 p = p.to_sym
61 p = p.to_sym
63 permissions << p unless permissions.include?(p)
62 permissions << p unless permissions.include?(p)
64 end
63 end
65 save!
64 save!
66 end
65 end
67
66
68 def remove_permission!(*perms)
67 def remove_permission!(*perms)
69 return unless permissions.is_a?(Array)
68 return unless permissions.is_a?(Array)
70 permissions_will_change!
69 permissions_will_change!
71 perms.each { |p| permissions.delete(p.to_sym) }
70 perms.each { |p| permissions.delete(p.to_sym) }
72 save!
71 save!
73 end
72 end
74
73
75 # Returns true if the role has the given permission
74 # Returns true if the role has the given permission
76 def has_permission?(perm)
75 def has_permission?(perm)
77 !permissions.nil? && permissions.include?(perm.to_sym)
76 !permissions.nil? && permissions.include?(perm.to_sym)
78 end
77 end
79
78
80 def <=>(role)
79 def <=>(role)
81 role ? position <=> role.position : -1
80 role ? position <=> role.position : -1
82 end
81 end
83
82
84 def to_s
83 def to_s
85 name
84 name
86 end
85 end
87
86
88 # Return true if the role is a builtin role
87 # Return true if the role is a builtin role
89 def builtin?
88 def builtin?
90 self.builtin != 0
89 self.builtin != 0
91 end
90 end
92
91
93 # Return true if the role is a project member role
92 # Return true if the role is a project member role
94 def member?
93 def member?
95 !self.builtin?
94 !self.builtin?
96 end
95 end
97
96
98 # Return true if role is allowed to do the specified action
97 # Return true if role is allowed to do the specified action
99 # action can be:
98 # action can be:
100 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
99 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
101 # * a permission Symbol (eg. :edit_project)
100 # * a permission Symbol (eg. :edit_project)
102 def allowed_to?(action)
101 def allowed_to?(action)
103 if action.is_a? Hash
102 if action.is_a? Hash
104 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
103 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
105 else
104 else
106 allowed_permissions.include? action
105 allowed_permissions.include? action
107 end
106 end
108 end
107 end
109
108
110 # Return all the permissions that can be given to the role
109 # Return all the permissions that can be given to the role
111 def setable_permissions
110 def setable_permissions
112 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
111 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
113 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
112 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
114 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
113 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
115 setable_permissions
114 setable_permissions
116 end
115 end
117
116
118 # Find all the roles that can be given to a project member
117 # Find all the roles that can be given to a project member
119 def self.find_all_givable
118 def self.find_all_givable
120 find(:all, :conditions => {:builtin => 0}, :order => 'position')
119 find(:all, :conditions => {:builtin => 0}, :order => 'position')
121 end
120 end
122
121
123 # Return the builtin 'non member' role. If the role doesn't exist,
122 # Return the builtin 'non member' role. If the role doesn't exist,
124 # it will be created on the fly.
123 # it will be created on the fly.
125 def self.non_member
124 def self.non_member
126 non_member_role = find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER})
125 non_member_role = find(:first, :conditions => {:builtin => BUILTIN_NON_MEMBER})
127 if non_member_role.nil?
126 if non_member_role.nil?
128 non_member_role = create(:name => 'Non member', :position => 0) do |role|
127 non_member_role = create(:name => 'Non member', :position => 0) do |role|
129 role.builtin = BUILTIN_NON_MEMBER
128 role.builtin = BUILTIN_NON_MEMBER
130 end
129 end
131 raise 'Unable to create the non-member role.' if non_member_role.new_record?
130 raise 'Unable to create the non-member role.' if non_member_role.new_record?
132 end
131 end
133 non_member_role
132 non_member_role
134 end
133 end
135
134
136 # Return the builtin 'anonymous' role. If the role doesn't exist,
135 # Return the builtin 'anonymous' role. If the role doesn't exist,
137 # it will be created on the fly.
136 # it will be created on the fly.
138 def self.anonymous
137 def self.anonymous
139 anonymous_role = find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS})
138 anonymous_role = find(:first, :conditions => {:builtin => BUILTIN_ANONYMOUS})
140 if anonymous_role.nil?
139 if anonymous_role.nil?
141 anonymous_role = create(:name => 'Anonymous', :position => 0) do |role|
140 anonymous_role = create(:name => 'Anonymous', :position => 0) do |role|
142 role.builtin = BUILTIN_ANONYMOUS
141 role.builtin = BUILTIN_ANONYMOUS
143 end
142 end
144 raise 'Unable to create the anonymous role.' if anonymous_role.new_record?
143 raise 'Unable to create the anonymous role.' if anonymous_role.new_record?
145 end
144 end
146 anonymous_role
145 anonymous_role
147 end
146 end
148
147
149
148
150 private
149 private
151 def allowed_permissions
150 def allowed_permissions
152 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
151 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
153 end
152 end
154
153
155 def allowed_actions
154 def allowed_actions
156 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
155 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
157 end
156 end
158
157
159 def check_deletable
158 def check_deletable
160 raise "Can't delete role" if members.any?
159 raise "Can't delete role" if members.any?
161 raise "Can't delete builtin role" if builtin?
160 raise "Can't delete builtin role" if builtin?
162 end
161 end
163 end
162 end
@@ -1,67 +1,66
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Tracker < ActiveRecord::Base
18 class Tracker < ActiveRecord::Base
19 before_destroy :check_integrity
19 before_destroy :check_integrity
20 has_many :issues
20 has_many :issues
21 has_many :workflows, :dependent => :delete_all do
21 has_many :workflows, :dependent => :delete_all do
22 def copy(source_tracker)
22 def copy(source_tracker)
23 Workflow.copy(source_tracker, nil, proxy_owner, nil)
23 Workflow.copy(source_tracker, nil, proxy_owner, nil)
24 end
24 end
25 end
25 end
26
26
27 has_and_belongs_to_many :projects
27 has_and_belongs_to_many :projects
28 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
28 has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
29 acts_as_list
29 acts_as_list
30
30
31 validates_presence_of :name
31 validates_presence_of :name
32 validates_uniqueness_of :name
32 validates_uniqueness_of :name
33 validates_length_of :name, :maximum => 30
33 validates_length_of :name, :maximum => 30
34 validates_format_of :name, :with => /^[\w\s\'\-]*$/i
35
34
36 def to_s; name end
35 def to_s; name end
37
36
38 def <=>(tracker)
37 def <=>(tracker)
39 name <=> tracker.name
38 name <=> tracker.name
40 end
39 end
41
40
42 def self.all
41 def self.all
43 find(:all, :order => 'position')
42 find(:all, :order => 'position')
44 end
43 end
45
44
46 # Returns an array of IssueStatus that are used
45 # Returns an array of IssueStatus that are used
47 # in the tracker's workflows
46 # in the tracker's workflows
48 def issue_statuses
47 def issue_statuses
49 if @issue_statuses
48 if @issue_statuses
50 return @issue_statuses
49 return @issue_statuses
51 elsif new_record?
50 elsif new_record?
52 return []
51 return []
53 end
52 end
54
53
55 ids = Workflow.
54 ids = Workflow.
56 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
55 connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
57 flatten.
56 flatten.
58 uniq
57 uniq
59
58
60 @issue_statuses = IssueStatus.find_all_by_id(ids).sort
59 @issue_statuses = IssueStatus.find_all_by_id(ids).sort
61 end
60 end
62
61
63 private
62 private
64 def check_integrity
63 def check_integrity
65 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
64 raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
66 end
65 end
67 end
66 end
@@ -1,502 +1,501
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Account statuses
23 # Account statuses
24 STATUS_ANONYMOUS = 0
24 STATUS_ANONYMOUS = 0
25 STATUS_ACTIVE = 1
25 STATUS_ACTIVE = 1
26 STATUS_REGISTERED = 2
26 STATUS_REGISTERED = 2
27 STATUS_LOCKED = 3
27 STATUS_LOCKED = 3
28
28
29 USER_FORMATS = {
29 USER_FORMATS = {
30 :firstname_lastname => '#{firstname} #{lastname}',
30 :firstname_lastname => '#{firstname} #{lastname}',
31 :firstname => '#{firstname}',
31 :firstname => '#{firstname}',
32 :lastname_firstname => '#{lastname} #{firstname}',
32 :lastname_firstname => '#{lastname} #{firstname}',
33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 :username => '#{login}'
34 :username => '#{login}'
35 }
35 }
36
36
37 MAIL_NOTIFICATION_OPTIONS = [
37 MAIL_NOTIFICATION_OPTIONS = [
38 ['all', :label_user_mail_option_all],
38 ['all', :label_user_mail_option_all],
39 ['selected', :label_user_mail_option_selected],
39 ['selected', :label_user_mail_option_selected],
40 ['only_my_events', :label_user_mail_option_only_my_events],
40 ['only_my_events', :label_user_mail_option_only_my_events],
41 ['only_assigned', :label_user_mail_option_only_assigned],
41 ['only_assigned', :label_user_mail_option_only_assigned],
42 ['only_owner', :label_user_mail_option_only_owner],
42 ['only_owner', :label_user_mail_option_only_owner],
43 ['none', :label_user_mail_option_none]
43 ['none', :label_user_mail_option_none]
44 ]
44 ]
45
45
46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
46 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
47 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
48 has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
49 has_many :changesets, :dependent => :nullify
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
51 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
51 has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
52 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
52 has_one :api_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='api'"
53 belongs_to :auth_source
53 belongs_to :auth_source
54
54
55 # Active non-anonymous users scope
55 # Active non-anonymous users scope
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
57
57
58 acts_as_customizable
58 acts_as_customizable
59
59
60 attr_accessor :password, :password_confirmation
60 attr_accessor :password, :password_confirmation
61 attr_accessor :last_before_login_on
61 attr_accessor :last_before_login_on
62 # Prevents unauthorized assignments
62 # Prevents unauthorized assignments
63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
64
64
65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
68 # Login must contain lettres, numbers, underscores only
68 # Login must contain lettres, numbers, underscores only
69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
70 validates_length_of :login, :maximum => 30
70 validates_length_of :login, :maximum => 30
71 validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
72 validates_length_of :firstname, :lastname, :maximum => 30
71 validates_length_of :firstname, :lastname, :maximum => 30
73 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
74 validates_length_of :mail, :maximum => 60, :allow_nil => true
73 validates_length_of :mail, :maximum => 60, :allow_nil => true
75 validates_confirmation_of :password, :allow_nil => true
74 validates_confirmation_of :password, :allow_nil => true
76 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
77
76
78 def before_create
77 def before_create
79 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
78 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
80 true
79 true
81 end
80 end
82
81
83 def before_save
82 def before_save
84 # update hashed_password if password was set
83 # update hashed_password if password was set
85 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
84 self.hashed_password = User.hash_password(self.password) if self.password && self.auth_source_id.blank?
86 end
85 end
87
86
88 def reload(*args)
87 def reload(*args)
89 @name = nil
88 @name = nil
90 super
89 super
91 end
90 end
92
91
93 def mail=(arg)
92 def mail=(arg)
94 write_attribute(:mail, arg.to_s.strip)
93 write_attribute(:mail, arg.to_s.strip)
95 end
94 end
96
95
97 def identity_url=(url)
96 def identity_url=(url)
98 if url.blank?
97 if url.blank?
99 write_attribute(:identity_url, '')
98 write_attribute(:identity_url, '')
100 else
99 else
101 begin
100 begin
102 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
101 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
103 rescue OpenIdAuthentication::InvalidOpenId
102 rescue OpenIdAuthentication::InvalidOpenId
104 # Invlaid url, don't save
103 # Invlaid url, don't save
105 end
104 end
106 end
105 end
107 self.read_attribute(:identity_url)
106 self.read_attribute(:identity_url)
108 end
107 end
109
108
110 # Returns the user that matches provided login and password, or nil
109 # Returns the user that matches provided login and password, or nil
111 def self.try_to_login(login, password)
110 def self.try_to_login(login, password)
112 # Make sure no one can sign in with an empty password
111 # Make sure no one can sign in with an empty password
113 return nil if password.to_s.empty?
112 return nil if password.to_s.empty?
114 user = find_by_login(login)
113 user = find_by_login(login)
115 if user
114 if user
116 # user is already in local database
115 # user is already in local database
117 return nil if !user.active?
116 return nil if !user.active?
118 if user.auth_source
117 if user.auth_source
119 # user has an external authentication method
118 # user has an external authentication method
120 return nil unless user.auth_source.authenticate(login, password)
119 return nil unless user.auth_source.authenticate(login, password)
121 else
120 else
122 # authentication with local password
121 # authentication with local password
123 return nil unless User.hash_password(password) == user.hashed_password
122 return nil unless User.hash_password(password) == user.hashed_password
124 end
123 end
125 else
124 else
126 # user is not yet registered, try to authenticate with available sources
125 # user is not yet registered, try to authenticate with available sources
127 attrs = AuthSource.authenticate(login, password)
126 attrs = AuthSource.authenticate(login, password)
128 if attrs
127 if attrs
129 user = new(attrs)
128 user = new(attrs)
130 user.login = login
129 user.login = login
131 user.language = Setting.default_language
130 user.language = Setting.default_language
132 if user.save
131 if user.save
133 user.reload
132 user.reload
134 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
133 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
135 end
134 end
136 end
135 end
137 end
136 end
138 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
137 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
139 user
138 user
140 rescue => text
139 rescue => text
141 raise text
140 raise text
142 end
141 end
143
142
144 # Returns the user who matches the given autologin +key+ or nil
143 # Returns the user who matches the given autologin +key+ or nil
145 def self.try_to_autologin(key)
144 def self.try_to_autologin(key)
146 tokens = Token.find_all_by_action_and_value('autologin', key)
145 tokens = Token.find_all_by_action_and_value('autologin', key)
147 # Make sure there's only 1 token that matches the key
146 # Make sure there's only 1 token that matches the key
148 if tokens.size == 1
147 if tokens.size == 1
149 token = tokens.first
148 token = tokens.first
150 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
149 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
151 token.user.update_attribute(:last_login_on, Time.now)
150 token.user.update_attribute(:last_login_on, Time.now)
152 token.user
151 token.user
153 end
152 end
154 end
153 end
155 end
154 end
156
155
157 # Return user's full name for display
156 # Return user's full name for display
158 def name(formatter = nil)
157 def name(formatter = nil)
159 if formatter
158 if formatter
160 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
159 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
161 else
160 else
162 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
161 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
163 end
162 end
164 end
163 end
165
164
166 def active?
165 def active?
167 self.status == STATUS_ACTIVE
166 self.status == STATUS_ACTIVE
168 end
167 end
169
168
170 def registered?
169 def registered?
171 self.status == STATUS_REGISTERED
170 self.status == STATUS_REGISTERED
172 end
171 end
173
172
174 def locked?
173 def locked?
175 self.status == STATUS_LOCKED
174 self.status == STATUS_LOCKED
176 end
175 end
177
176
178 def activate
177 def activate
179 self.status = STATUS_ACTIVE
178 self.status = STATUS_ACTIVE
180 end
179 end
181
180
182 def register
181 def register
183 self.status = STATUS_REGISTERED
182 self.status = STATUS_REGISTERED
184 end
183 end
185
184
186 def lock
185 def lock
187 self.status = STATUS_LOCKED
186 self.status = STATUS_LOCKED
188 end
187 end
189
188
190 def activate!
189 def activate!
191 update_attribute(:status, STATUS_ACTIVE)
190 update_attribute(:status, STATUS_ACTIVE)
192 end
191 end
193
192
194 def register!
193 def register!
195 update_attribute(:status, STATUS_REGISTERED)
194 update_attribute(:status, STATUS_REGISTERED)
196 end
195 end
197
196
198 def lock!
197 def lock!
199 update_attribute(:status, STATUS_LOCKED)
198 update_attribute(:status, STATUS_LOCKED)
200 end
199 end
201
200
202 def check_password?(clear_password)
201 def check_password?(clear_password)
203 if auth_source_id.present?
202 if auth_source_id.present?
204 auth_source.authenticate(self.login, clear_password)
203 auth_source.authenticate(self.login, clear_password)
205 else
204 else
206 User.hash_password(clear_password) == self.hashed_password
205 User.hash_password(clear_password) == self.hashed_password
207 end
206 end
208 end
207 end
209
208
210 # Does the backend storage allow this user to change their password?
209 # Does the backend storage allow this user to change their password?
211 def change_password_allowed?
210 def change_password_allowed?
212 return true if auth_source_id.blank?
211 return true if auth_source_id.blank?
213 return auth_source.allow_password_changes?
212 return auth_source.allow_password_changes?
214 end
213 end
215
214
216 # Generate and set a random password. Useful for automated user creation
215 # Generate and set a random password. Useful for automated user creation
217 # Based on Token#generate_token_value
216 # Based on Token#generate_token_value
218 #
217 #
219 def random_password
218 def random_password
220 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
219 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
221 password = ''
220 password = ''
222 40.times { |i| password << chars[rand(chars.size-1)] }
221 40.times { |i| password << chars[rand(chars.size-1)] }
223 self.password = password
222 self.password = password
224 self.password_confirmation = password
223 self.password_confirmation = password
225 self
224 self
226 end
225 end
227
226
228 def pref
227 def pref
229 self.preference ||= UserPreference.new(:user => self)
228 self.preference ||= UserPreference.new(:user => self)
230 end
229 end
231
230
232 def time_zone
231 def time_zone
233 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
232 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
234 end
233 end
235
234
236 def wants_comments_in_reverse_order?
235 def wants_comments_in_reverse_order?
237 self.pref[:comments_sorting] == 'desc'
236 self.pref[:comments_sorting] == 'desc'
238 end
237 end
239
238
240 # Return user's RSS key (a 40 chars long string), used to access feeds
239 # Return user's RSS key (a 40 chars long string), used to access feeds
241 def rss_key
240 def rss_key
242 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
241 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
243 token.value
242 token.value
244 end
243 end
245
244
246 # Return user's API key (a 40 chars long string), used to access the API
245 # Return user's API key (a 40 chars long string), used to access the API
247 def api_key
246 def api_key
248 token = self.api_token || self.create_api_token(:action => 'api')
247 token = self.api_token || self.create_api_token(:action => 'api')
249 token.value
248 token.value
250 end
249 end
251
250
252 # Return an array of project ids for which the user has explicitly turned mail notifications on
251 # Return an array of project ids for which the user has explicitly turned mail notifications on
253 def notified_projects_ids
252 def notified_projects_ids
254 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
253 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
255 end
254 end
256
255
257 def notified_project_ids=(ids)
256 def notified_project_ids=(ids)
258 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
257 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
259 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
258 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
260 @notified_projects_ids = nil
259 @notified_projects_ids = nil
261 notified_projects_ids
260 notified_projects_ids
262 end
261 end
263
262
264 # Only users that belong to more than 1 project can select projects for which they are notified
263 # Only users that belong to more than 1 project can select projects for which they are notified
265 def valid_notification_options
264 def valid_notification_options
266 # Note that @user.membership.size would fail since AR ignores
265 # Note that @user.membership.size would fail since AR ignores
267 # :include association option when doing a count
266 # :include association option when doing a count
268 if memberships.length < 1
267 if memberships.length < 1
269 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == 'selected'}
268 MAIL_NOTIFICATION_OPTIONS.delete_if {|option| option.first == 'selected'}
270 else
269 else
271 MAIL_NOTIFICATION_OPTIONS
270 MAIL_NOTIFICATION_OPTIONS
272 end
271 end
273 end
272 end
274
273
275 # Find a user account by matching the exact login and then a case-insensitive
274 # Find a user account by matching the exact login and then a case-insensitive
276 # version. Exact matches will be given priority.
275 # version. Exact matches will be given priority.
277 def self.find_by_login(login)
276 def self.find_by_login(login)
278 # force string comparison to be case sensitive on MySQL
277 # force string comparison to be case sensitive on MySQL
279 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
278 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
280
279
281 # First look for an exact match
280 # First look for an exact match
282 user = first(:conditions => ["#{type_cast} login = ?", login])
281 user = first(:conditions => ["#{type_cast} login = ?", login])
283 # Fail over to case-insensitive if none was found
282 # Fail over to case-insensitive if none was found
284 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
283 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
285 end
284 end
286
285
287 def self.find_by_rss_key(key)
286 def self.find_by_rss_key(key)
288 token = Token.find_by_value(key)
287 token = Token.find_by_value(key)
289 token && token.user.active? ? token.user : nil
288 token && token.user.active? ? token.user : nil
290 end
289 end
291
290
292 def self.find_by_api_key(key)
291 def self.find_by_api_key(key)
293 token = Token.find_by_action_and_value('api', key)
292 token = Token.find_by_action_and_value('api', key)
294 token && token.user.active? ? token.user : nil
293 token && token.user.active? ? token.user : nil
295 end
294 end
296
295
297 # Makes find_by_mail case-insensitive
296 # Makes find_by_mail case-insensitive
298 def self.find_by_mail(mail)
297 def self.find_by_mail(mail)
299 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
298 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
300 end
299 end
301
300
302 def to_s
301 def to_s
303 name
302 name
304 end
303 end
305
304
306 # Returns the current day according to user's time zone
305 # Returns the current day according to user's time zone
307 def today
306 def today
308 if time_zone.nil?
307 if time_zone.nil?
309 Date.today
308 Date.today
310 else
309 else
311 Time.now.in_time_zone(time_zone).to_date
310 Time.now.in_time_zone(time_zone).to_date
312 end
311 end
313 end
312 end
314
313
315 def logged?
314 def logged?
316 true
315 true
317 end
316 end
318
317
319 def anonymous?
318 def anonymous?
320 !logged?
319 !logged?
321 end
320 end
322
321
323 # Return user's roles for project
322 # Return user's roles for project
324 def roles_for_project(project)
323 def roles_for_project(project)
325 roles = []
324 roles = []
326 # No role on archived projects
325 # No role on archived projects
327 return roles unless project && project.active?
326 return roles unless project && project.active?
328 if logged?
327 if logged?
329 # Find project membership
328 # Find project membership
330 membership = memberships.detect {|m| m.project_id == project.id}
329 membership = memberships.detect {|m| m.project_id == project.id}
331 if membership
330 if membership
332 roles = membership.roles
331 roles = membership.roles
333 else
332 else
334 @role_non_member ||= Role.non_member
333 @role_non_member ||= Role.non_member
335 roles << @role_non_member
334 roles << @role_non_member
336 end
335 end
337 else
336 else
338 @role_anonymous ||= Role.anonymous
337 @role_anonymous ||= Role.anonymous
339 roles << @role_anonymous
338 roles << @role_anonymous
340 end
339 end
341 roles
340 roles
342 end
341 end
343
342
344 # Return true if the user is a member of project
343 # Return true if the user is a member of project
345 def member_of?(project)
344 def member_of?(project)
346 !roles_for_project(project).detect {|role| role.member?}.nil?
345 !roles_for_project(project).detect {|role| role.member?}.nil?
347 end
346 end
348
347
349 # Return true if the user is allowed to do the specified action on a specific context
348 # Return true if the user is allowed to do the specified action on a specific context
350 # Action can be:
349 # Action can be:
351 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
350 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
352 # * a permission Symbol (eg. :edit_project)
351 # * a permission Symbol (eg. :edit_project)
353 # Context can be:
352 # Context can be:
354 # * a project : returns true if user is allowed to do the specified action on this project
353 # * a project : returns true if user is allowed to do the specified action on this project
355 # * a group of projects : returns true if user is allowed on every project
354 # * a group of projects : returns true if user is allowed on every project
356 # * nil with options[:global] set : check if user has at least one role allowed for this action,
355 # * nil with options[:global] set : check if user has at least one role allowed for this action,
357 # or falls back to Non Member / Anonymous permissions depending if the user is logged
356 # or falls back to Non Member / Anonymous permissions depending if the user is logged
358 def allowed_to?(action, context, options={})
357 def allowed_to?(action, context, options={})
359 if context && context.is_a?(Project)
358 if context && context.is_a?(Project)
360 # No action allowed on archived projects
359 # No action allowed on archived projects
361 return false unless context.active?
360 return false unless context.active?
362 # No action allowed on disabled modules
361 # No action allowed on disabled modules
363 return false unless context.allows_to?(action)
362 return false unless context.allows_to?(action)
364 # Admin users are authorized for anything else
363 # Admin users are authorized for anything else
365 return true if admin?
364 return true if admin?
366
365
367 roles = roles_for_project(context)
366 roles = roles_for_project(context)
368 return false unless roles
367 return false unless roles
369 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
368 roles.detect {|role| (context.is_public? || role.member?) && role.allowed_to?(action)}
370
369
371 elsif context && context.is_a?(Array)
370 elsif context && context.is_a?(Array)
372 # Authorize if user is authorized on every element of the array
371 # Authorize if user is authorized on every element of the array
373 context.map do |project|
372 context.map do |project|
374 allowed_to?(action,project,options)
373 allowed_to?(action,project,options)
375 end.inject do |memo,allowed|
374 end.inject do |memo,allowed|
376 memo && allowed
375 memo && allowed
377 end
376 end
378 elsif options[:global]
377 elsif options[:global]
379 # Admin users are always authorized
378 # Admin users are always authorized
380 return true if admin?
379 return true if admin?
381
380
382 # authorize if user has at least one role that has this permission
381 # authorize if user has at least one role that has this permission
383 roles = memberships.collect {|m| m.roles}.flatten.uniq
382 roles = memberships.collect {|m| m.roles}.flatten.uniq
384 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
383 roles.detect {|r| r.allowed_to?(action)} || (self.logged? ? Role.non_member.allowed_to?(action) : Role.anonymous.allowed_to?(action))
385 else
384 else
386 false
385 false
387 end
386 end
388 end
387 end
389
388
390 # Is the user allowed to do the specified action on any project?
389 # Is the user allowed to do the specified action on any project?
391 # See allowed_to? for the actions and valid options.
390 # See allowed_to? for the actions and valid options.
392 def allowed_to_globally?(action, options)
391 def allowed_to_globally?(action, options)
393 allowed_to?(action, nil, options.reverse_merge(:global => true))
392 allowed_to?(action, nil, options.reverse_merge(:global => true))
394 end
393 end
395
394
396 safe_attributes 'login',
395 safe_attributes 'login',
397 'firstname',
396 'firstname',
398 'lastname',
397 'lastname',
399 'mail',
398 'mail',
400 'mail_notification',
399 'mail_notification',
401 'language',
400 'language',
402 'custom_field_values',
401 'custom_field_values',
403 'custom_fields',
402 'custom_fields',
404 'identity_url'
403 'identity_url'
405
404
406 safe_attributes 'status',
405 safe_attributes 'status',
407 'auth_source_id',
406 'auth_source_id',
408 :if => lambda {|user, current_user| current_user.admin?}
407 :if => lambda {|user, current_user| current_user.admin?}
409
408
410 safe_attributes 'group_ids',
409 safe_attributes 'group_ids',
411 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
410 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
412
411
413 # Utility method to help check if a user should be notified about an
412 # Utility method to help check if a user should be notified about an
414 # event.
413 # event.
415 #
414 #
416 # TODO: only supports Issue events currently
415 # TODO: only supports Issue events currently
417 def notify_about?(object)
416 def notify_about?(object)
418 case mail_notification
417 case mail_notification
419 when 'all'
418 when 'all'
420 true
419 true
421 when 'selected'
420 when 'selected'
422 # Handled by the Project
421 # Handled by the Project
423 when 'none'
422 when 'none'
424 false
423 false
425 when 'only_my_events'
424 when 'only_my_events'
426 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
425 if object.is_a?(Issue) && (object.author == self || object.assigned_to == self)
427 true
426 true
428 else
427 else
429 false
428 false
430 end
429 end
431 when 'only_assigned'
430 when 'only_assigned'
432 if object.is_a?(Issue) && object.assigned_to == self
431 if object.is_a?(Issue) && object.assigned_to == self
433 true
432 true
434 else
433 else
435 false
434 false
436 end
435 end
437 when 'only_owner'
436 when 'only_owner'
438 if object.is_a?(Issue) && object.author == self
437 if object.is_a?(Issue) && object.author == self
439 true
438 true
440 else
439 else
441 false
440 false
442 end
441 end
443 else
442 else
444 false
443 false
445 end
444 end
446 end
445 end
447
446
448 def self.current=(user)
447 def self.current=(user)
449 @current_user = user
448 @current_user = user
450 end
449 end
451
450
452 def self.current
451 def self.current
453 @current_user ||= User.anonymous
452 @current_user ||= User.anonymous
454 end
453 end
455
454
456 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
455 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
457 # one anonymous user per database.
456 # one anonymous user per database.
458 def self.anonymous
457 def self.anonymous
459 anonymous_user = AnonymousUser.find(:first)
458 anonymous_user = AnonymousUser.find(:first)
460 if anonymous_user.nil?
459 if anonymous_user.nil?
461 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
460 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
462 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
461 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
463 end
462 end
464 anonymous_user
463 anonymous_user
465 end
464 end
466
465
467 protected
466 protected
468
467
469 def validate
468 def validate
470 # Password length validation based on setting
469 # Password length validation based on setting
471 if !password.nil? && password.size < Setting.password_min_length.to_i
470 if !password.nil? && password.size < Setting.password_min_length.to_i
472 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
471 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
473 end
472 end
474 end
473 end
475
474
476 private
475 private
477
476
478 # Return password digest
477 # Return password digest
479 def self.hash_password(clear_password)
478 def self.hash_password(clear_password)
480 Digest::SHA1.hexdigest(clear_password || "")
479 Digest::SHA1.hexdigest(clear_password || "")
481 end
480 end
482 end
481 end
483
482
484 class AnonymousUser < User
483 class AnonymousUser < User
485
484
486 def validate_on_create
485 def validate_on_create
487 # There should be only one AnonymousUser in the database
486 # There should be only one AnonymousUser in the database
488 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
487 errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
489 end
488 end
490
489
491 def available_custom_fields
490 def available_custom_fields
492 []
491 []
493 end
492 end
494
493
495 # Overrides a few properties
494 # Overrides a few properties
496 def logged?; false end
495 def logged?; false end
497 def admin; false end
496 def admin; false end
498 def name(*args); I18n.t(:label_user_anonymous) end
497 def name(*args); I18n.t(:label_user_anonymous) end
499 def mail; nil end
498 def mail; nil end
500 def time_zone; nil end
499 def time_zone; nil end
501 def rss_key; nil end
500 def rss_key; nil end
502 end
501 end
@@ -1,514 +1,512
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 desc 'Mantis migration script'
18 desc 'Mantis migration script'
19
19
20 require 'active_record'
20 require 'active_record'
21 require 'iconv'
21 require 'iconv'
22 require 'pp'
22 require 'pp'
23
23
24 namespace :redmine do
24 namespace :redmine do
25 task :migrate_from_mantis => :environment do
25 task :migrate_from_mantis => :environment do
26
26
27 module MantisMigrate
27 module MantisMigrate
28
28
29 DEFAULT_STATUS = IssueStatus.default
29 DEFAULT_STATUS = IssueStatus.default
30 assigned_status = IssueStatus.find_by_position(2)
30 assigned_status = IssueStatus.find_by_position(2)
31 resolved_status = IssueStatus.find_by_position(3)
31 resolved_status = IssueStatus.find_by_position(3)
32 feedback_status = IssueStatus.find_by_position(4)
32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
34 STATUS_MAPPING = {10 => DEFAULT_STATUS, # new
35 20 => feedback_status, # feedback
35 20 => feedback_status, # feedback
36 30 => DEFAULT_STATUS, # acknowledged
36 30 => DEFAULT_STATUS, # acknowledged
37 40 => DEFAULT_STATUS, # confirmed
37 40 => DEFAULT_STATUS, # confirmed
38 50 => assigned_status, # assigned
38 50 => assigned_status, # assigned
39 80 => resolved_status, # resolved
39 80 => resolved_status, # resolved
40 90 => closed_status # closed
40 90 => closed_status # closed
41 }
41 }
42
42
43 priorities = IssuePriority.all
43 priorities = IssuePriority.all
44 DEFAULT_PRIORITY = priorities[2]
44 DEFAULT_PRIORITY = priorities[2]
45 PRIORITY_MAPPING = {10 => priorities[1], # none
45 PRIORITY_MAPPING = {10 => priorities[1], # none
46 20 => priorities[1], # low
46 20 => priorities[1], # low
47 30 => priorities[2], # normal
47 30 => priorities[2], # normal
48 40 => priorities[3], # high
48 40 => priorities[3], # high
49 50 => priorities[4], # urgent
49 50 => priorities[4], # urgent
50 60 => priorities[5] # immediate
50 60 => priorities[5] # immediate
51 }
51 }
52
52
53 TRACKER_BUG = Tracker.find_by_position(1)
53 TRACKER_BUG = Tracker.find_by_position(1)
54 TRACKER_FEATURE = Tracker.find_by_position(2)
54 TRACKER_FEATURE = Tracker.find_by_position(2)
55
55
56 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
56 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
57 manager_role = roles[0]
57 manager_role = roles[0]
58 developer_role = roles[1]
58 developer_role = roles[1]
59 DEFAULT_ROLE = roles.last
59 DEFAULT_ROLE = roles.last
60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
60 ROLE_MAPPING = {10 => DEFAULT_ROLE, # viewer
61 25 => DEFAULT_ROLE, # reporter
61 25 => DEFAULT_ROLE, # reporter
62 40 => DEFAULT_ROLE, # updater
62 40 => DEFAULT_ROLE, # updater
63 55 => developer_role, # developer
63 55 => developer_role, # developer
64 70 => manager_role, # manager
64 70 => manager_role, # manager
65 90 => manager_role # administrator
65 90 => manager_role # administrator
66 }
66 }
67
67
68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
68 CUSTOM_FIELD_TYPE_MAPPING = {0 => 'string', # String
69 1 => 'int', # Numeric
69 1 => 'int', # Numeric
70 2 => 'int', # Float
70 2 => 'int', # Float
71 3 => 'list', # Enumeration
71 3 => 'list', # Enumeration
72 4 => 'string', # Email
72 4 => 'string', # Email
73 5 => 'bool', # Checkbox
73 5 => 'bool', # Checkbox
74 6 => 'list', # List
74 6 => 'list', # List
75 7 => 'list', # Multiselection list
75 7 => 'list', # Multiselection list
76 8 => 'date', # Date
76 8 => 'date', # Date
77 }
77 }
78
78
79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
79 RELATION_TYPE_MAPPING = {1 => IssueRelation::TYPE_RELATES, # related to
80 2 => IssueRelation::TYPE_RELATES, # parent of
80 2 => IssueRelation::TYPE_RELATES, # parent of
81 3 => IssueRelation::TYPE_RELATES, # child of
81 3 => IssueRelation::TYPE_RELATES, # child of
82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
82 0 => IssueRelation::TYPE_DUPLICATES, # duplicate of
83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
83 4 => IssueRelation::TYPE_DUPLICATES # has duplicate
84 }
84 }
85
85
86 class MantisUser < ActiveRecord::Base
86 class MantisUser < ActiveRecord::Base
87 set_table_name :mantis_user_table
87 set_table_name :mantis_user_table
88
88
89 def firstname
89 def firstname
90 @firstname = realname.blank? ? username : realname.split.first[0..29]
90 @firstname = realname.blank? ? username : realname.split.first[0..29]
91 @firstname.gsub!(/[^\w\s\'\-]/i, '')
92 @firstname
91 @firstname
93 end
92 end
94
93
95 def lastname
94 def lastname
96 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
97 @lastname.gsub!(/[^\w\s\'\-]/i, '')
98 @lastname = '-' if @lastname.blank?
96 @lastname = '-' if @lastname.blank?
99 @lastname
97 @lastname
100 end
98 end
101
99
102 def email
100 def email
103 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
101 if read_attribute(:email).match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) &&
104 !User.find_by_mail(read_attribute(:email))
102 !User.find_by_mail(read_attribute(:email))
105 @email = read_attribute(:email)
103 @email = read_attribute(:email)
106 else
104 else
107 @email = "#{username}@foo.bar"
105 @email = "#{username}@foo.bar"
108 end
106 end
109 end
107 end
110
108
111 def username
109 def username
112 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
113 end
111 end
114 end
112 end
115
113
116 class MantisProject < ActiveRecord::Base
114 class MantisProject < ActiveRecord::Base
117 set_table_name :mantis_project_table
115 set_table_name :mantis_project_table
118 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
119 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
120 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
121 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
122
120
123 def identifier
121 def identifier
124 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
122 read_attribute(:name).gsub(/[^a-z0-9\-]+/, '-').slice(0, Project::IDENTIFIER_MAX_LENGTH)
125 end
123 end
126 end
124 end
127
125
128 class MantisVersion < ActiveRecord::Base
126 class MantisVersion < ActiveRecord::Base
129 set_table_name :mantis_project_version_table
127 set_table_name :mantis_project_version_table
130
128
131 def version
129 def version
132 read_attribute(:version)[0..29]
130 read_attribute(:version)[0..29]
133 end
131 end
134
132
135 def description
133 def description
136 read_attribute(:description)[0..254]
134 read_attribute(:description)[0..254]
137 end
135 end
138 end
136 end
139
137
140 class MantisCategory < ActiveRecord::Base
138 class MantisCategory < ActiveRecord::Base
141 set_table_name :mantis_project_category_table
139 set_table_name :mantis_project_category_table
142 end
140 end
143
141
144 class MantisProjectUser < ActiveRecord::Base
142 class MantisProjectUser < ActiveRecord::Base
145 set_table_name :mantis_project_user_list_table
143 set_table_name :mantis_project_user_list_table
146 end
144 end
147
145
148 class MantisBug < ActiveRecord::Base
146 class MantisBug < ActiveRecord::Base
149 set_table_name :mantis_bug_table
147 set_table_name :mantis_bug_table
150 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
148 belongs_to :bug_text, :class_name => "MantisBugText", :foreign_key => :bug_text_id
151 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
152 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
153 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
154 end
152 end
155
153
156 class MantisBugText < ActiveRecord::Base
154 class MantisBugText < ActiveRecord::Base
157 set_table_name :mantis_bug_text_table
155 set_table_name :mantis_bug_text_table
158
156
159 # Adds Mantis steps_to_reproduce and additional_information fields
157 # Adds Mantis steps_to_reproduce and additional_information fields
160 # to description if any
158 # to description if any
161 def full_description
159 def full_description
162 full_description = description
160 full_description = description
163 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
161 full_description += "\n\n*Steps to reproduce:*\n\n#{steps_to_reproduce}" unless steps_to_reproduce.blank?
164 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
162 full_description += "\n\n*Additional information:*\n\n#{additional_information}" unless additional_information.blank?
165 full_description
163 full_description
166 end
164 end
167 end
165 end
168
166
169 class MantisBugNote < ActiveRecord::Base
167 class MantisBugNote < ActiveRecord::Base
170 set_table_name :mantis_bugnote_table
168 set_table_name :mantis_bugnote_table
171 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
172 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
170 belongs_to :bug_note_text, :class_name => "MantisBugNoteText", :foreign_key => :bugnote_text_id
173 end
171 end
174
172
175 class MantisBugNoteText < ActiveRecord::Base
173 class MantisBugNoteText < ActiveRecord::Base
176 set_table_name :mantis_bugnote_text_table
174 set_table_name :mantis_bugnote_text_table
177 end
175 end
178
176
179 class MantisBugFile < ActiveRecord::Base
177 class MantisBugFile < ActiveRecord::Base
180 set_table_name :mantis_bug_file_table
178 set_table_name :mantis_bug_file_table
181
179
182 def size
180 def size
183 filesize
181 filesize
184 end
182 end
185
183
186 def original_filename
184 def original_filename
187 MantisMigrate.encode(filename)
185 MantisMigrate.encode(filename)
188 end
186 end
189
187
190 def content_type
188 def content_type
191 file_type
189 file_type
192 end
190 end
193
191
194 def read(*args)
192 def read(*args)
195 if @read_finished
193 if @read_finished
196 nil
194 nil
197 else
195 else
198 @read_finished = true
196 @read_finished = true
199 content
197 content
200 end
198 end
201 end
199 end
202 end
200 end
203
201
204 class MantisBugRelationship < ActiveRecord::Base
202 class MantisBugRelationship < ActiveRecord::Base
205 set_table_name :mantis_bug_relationship_table
203 set_table_name :mantis_bug_relationship_table
206 end
204 end
207
205
208 class MantisBugMonitor < ActiveRecord::Base
206 class MantisBugMonitor < ActiveRecord::Base
209 set_table_name :mantis_bug_monitor_table
207 set_table_name :mantis_bug_monitor_table
210 end
208 end
211
209
212 class MantisNews < ActiveRecord::Base
210 class MantisNews < ActiveRecord::Base
213 set_table_name :mantis_news_table
211 set_table_name :mantis_news_table
214 end
212 end
215
213
216 class MantisCustomField < ActiveRecord::Base
214 class MantisCustomField < ActiveRecord::Base
217 set_table_name :mantis_custom_field_table
215 set_table_name :mantis_custom_field_table
218 set_inheritance_column :none
216 set_inheritance_column :none
219 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
220 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
221
219
222 def format
220 def format
223 read_attribute :type
221 read_attribute :type
224 end
222 end
225
223
226 def name
224 def name
227 read_attribute(:name)[0..29].gsub(/[^\w\s\'\-]/, '-')
225 read_attribute(:name)[0..29]
228 end
226 end
229 end
227 end
230
228
231 class MantisCustomFieldProject < ActiveRecord::Base
229 class MantisCustomFieldProject < ActiveRecord::Base
232 set_table_name :mantis_custom_field_project_table
230 set_table_name :mantis_custom_field_project_table
233 end
231 end
234
232
235 class MantisCustomFieldString < ActiveRecord::Base
233 class MantisCustomFieldString < ActiveRecord::Base
236 set_table_name :mantis_custom_field_string_table
234 set_table_name :mantis_custom_field_string_table
237 end
235 end
238
236
239
237
240 def self.migrate
238 def self.migrate
241
239
242 # Users
240 # Users
243 print "Migrating users"
241 print "Migrating users"
244 User.delete_all "login <> 'admin'"
242 User.delete_all "login <> 'admin'"
245 users_map = {}
243 users_map = {}
246 users_migrated = 0
244 users_migrated = 0
247 MantisUser.find(:all).each do |user|
245 MantisUser.find(:all).each do |user|
248 u = User.new :firstname => encode(user.firstname),
246 u = User.new :firstname => encode(user.firstname),
249 :lastname => encode(user.lastname),
247 :lastname => encode(user.lastname),
250 :mail => user.email,
248 :mail => user.email,
251 :last_login_on => user.last_visit
249 :last_login_on => user.last_visit
252 u.login = user.username
250 u.login = user.username
253 u.password = 'mantis'
251 u.password = 'mantis'
254 u.status = User::STATUS_LOCKED if user.enabled != 1
252 u.status = User::STATUS_LOCKED if user.enabled != 1
255 u.admin = true if user.access_level == 90
253 u.admin = true if user.access_level == 90
256 next unless u.save!
254 next unless u.save!
257 users_migrated += 1
255 users_migrated += 1
258 users_map[user.id] = u.id
256 users_map[user.id] = u.id
259 print '.'
257 print '.'
260 end
258 end
261 puts
259 puts
262
260
263 # Projects
261 # Projects
264 print "Migrating projects"
262 print "Migrating projects"
265 Project.destroy_all
263 Project.destroy_all
266 projects_map = {}
264 projects_map = {}
267 versions_map = {}
265 versions_map = {}
268 categories_map = {}
266 categories_map = {}
269 MantisProject.find(:all).each do |project|
267 MantisProject.find(:all).each do |project|
270 p = Project.new :name => encode(project.name),
268 p = Project.new :name => encode(project.name),
271 :description => encode(project.description)
269 :description => encode(project.description)
272 p.identifier = project.identifier
270 p.identifier = project.identifier
273 next unless p.save
271 next unless p.save
274 projects_map[project.id] = p.id
272 projects_map[project.id] = p.id
275 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
273 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
276 p.trackers << TRACKER_BUG
274 p.trackers << TRACKER_BUG
277 p.trackers << TRACKER_FEATURE
275 p.trackers << TRACKER_FEATURE
278 print '.'
276 print '.'
279
277
280 # Project members
278 # Project members
281 project.members.each do |member|
279 project.members.each do |member|
282 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
280 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
283 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
281 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
284 m.project = p
282 m.project = p
285 m.save
283 m.save
286 end
284 end
287
285
288 # Project versions
286 # Project versions
289 project.versions.each do |version|
287 project.versions.each do |version|
290 v = Version.new :name => encode(version.version),
288 v = Version.new :name => encode(version.version),
291 :description => encode(version.description),
289 :description => encode(version.description),
292 :effective_date => version.date_order.to_date
290 :effective_date => version.date_order.to_date
293 v.project = p
291 v.project = p
294 v.save
292 v.save
295 versions_map[version.id] = v.id
293 versions_map[version.id] = v.id
296 end
294 end
297
295
298 # Project categories
296 # Project categories
299 project.categories.each do |category|
297 project.categories.each do |category|
300 g = IssueCategory.new :name => category.category[0,30]
298 g = IssueCategory.new :name => category.category[0,30]
301 g.project = p
299 g.project = p
302 g.save
300 g.save
303 categories_map[category.category] = g.id
301 categories_map[category.category] = g.id
304 end
302 end
305 end
303 end
306 puts
304 puts
307
305
308 # Bugs
306 # Bugs
309 print "Migrating bugs"
307 print "Migrating bugs"
310 Issue.destroy_all
308 Issue.destroy_all
311 issues_map = {}
309 issues_map = {}
312 keep_bug_ids = (Issue.count == 0)
310 keep_bug_ids = (Issue.count == 0)
313 MantisBug.find_each(:batch_size => 200) do |bug|
311 MantisBug.find_each(:batch_size => 200) do |bug|
314 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
312 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
315 i = Issue.new :project_id => projects_map[bug.project_id],
313 i = Issue.new :project_id => projects_map[bug.project_id],
316 :subject => encode(bug.summary),
314 :subject => encode(bug.summary),
317 :description => encode(bug.bug_text.full_description),
315 :description => encode(bug.bug_text.full_description),
318 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
316 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
319 :created_on => bug.date_submitted,
317 :created_on => bug.date_submitted,
320 :updated_on => bug.last_updated
318 :updated_on => bug.last_updated
321 i.author = User.find_by_id(users_map[bug.reporter_id])
319 i.author = User.find_by_id(users_map[bug.reporter_id])
322 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
320 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
323 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
321 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
324 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
322 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
325 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
323 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
326 i.id = bug.id if keep_bug_ids
324 i.id = bug.id if keep_bug_ids
327 next unless i.save
325 next unless i.save
328 issues_map[bug.id] = i.id
326 issues_map[bug.id] = i.id
329 print '.'
327 print '.'
330 STDOUT.flush
328 STDOUT.flush
331
329
332 # Assignee
330 # Assignee
333 # Redmine checks that the assignee is a project member
331 # Redmine checks that the assignee is a project member
334 if (bug.handler_id && users_map[bug.handler_id])
332 if (bug.handler_id && users_map[bug.handler_id])
335 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
333 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
336 i.save_with_validation(false)
334 i.save_with_validation(false)
337 end
335 end
338
336
339 # Bug notes
337 # Bug notes
340 bug.bug_notes.each do |note|
338 bug.bug_notes.each do |note|
341 next unless users_map[note.reporter_id]
339 next unless users_map[note.reporter_id]
342 n = Journal.new :notes => encode(note.bug_note_text.note),
340 n = Journal.new :notes => encode(note.bug_note_text.note),
343 :created_on => note.date_submitted
341 :created_on => note.date_submitted
344 n.user = User.find_by_id(users_map[note.reporter_id])
342 n.user = User.find_by_id(users_map[note.reporter_id])
345 n.journalized = i
343 n.journalized = i
346 n.save
344 n.save
347 end
345 end
348
346
349 # Bug files
347 # Bug files
350 bug.bug_files.each do |file|
348 bug.bug_files.each do |file|
351 a = Attachment.new :created_on => file.date_added
349 a = Attachment.new :created_on => file.date_added
352 a.file = file
350 a.file = file
353 a.author = User.find :first
351 a.author = User.find :first
354 a.container = i
352 a.container = i
355 a.save
353 a.save
356 end
354 end
357
355
358 # Bug monitors
356 # Bug monitors
359 bug.bug_monitors.each do |monitor|
357 bug.bug_monitors.each do |monitor|
360 next unless users_map[monitor.user_id]
358 next unless users_map[monitor.user_id]
361 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
359 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
362 end
360 end
363 end
361 end
364
362
365 # update issue id sequence if needed (postgresql)
363 # update issue id sequence if needed (postgresql)
366 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
364 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
367 puts
365 puts
368
366
369 # Bug relationships
367 # Bug relationships
370 print "Migrating bug relations"
368 print "Migrating bug relations"
371 MantisBugRelationship.find(:all).each do |relation|
369 MantisBugRelationship.find(:all).each do |relation|
372 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
370 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
373 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
371 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
374 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
372 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
375 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
373 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
376 pp r unless r.save
374 pp r unless r.save
377 print '.'
375 print '.'
378 STDOUT.flush
376 STDOUT.flush
379 end
377 end
380 puts
378 puts
381
379
382 # News
380 # News
383 print "Migrating news"
381 print "Migrating news"
384 News.destroy_all
382 News.destroy_all
385 MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|
383 MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|
386 next unless projects_map[news.project_id]
384 next unless projects_map[news.project_id]
387 n = News.new :project_id => projects_map[news.project_id],
385 n = News.new :project_id => projects_map[news.project_id],
388 :title => encode(news.headline[0..59]),
386 :title => encode(news.headline[0..59]),
389 :description => encode(news.body),
387 :description => encode(news.body),
390 :created_on => news.date_posted
388 :created_on => news.date_posted
391 n.author = User.find_by_id(users_map[news.poster_id])
389 n.author = User.find_by_id(users_map[news.poster_id])
392 n.save
390 n.save
393 print '.'
391 print '.'
394 STDOUT.flush
392 STDOUT.flush
395 end
393 end
396 puts
394 puts
397
395
398 # Custom fields
396 # Custom fields
399 print "Migrating custom fields"
397 print "Migrating custom fields"
400 IssueCustomField.destroy_all
398 IssueCustomField.destroy_all
401 MantisCustomField.find(:all).each do |field|
399 MantisCustomField.find(:all).each do |field|
402 f = IssueCustomField.new :name => field.name[0..29],
400 f = IssueCustomField.new :name => field.name[0..29],
403 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
401 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
404 :min_length => field.length_min,
402 :min_length => field.length_min,
405 :max_length => field.length_max,
403 :max_length => field.length_max,
406 :regexp => field.valid_regexp,
404 :regexp => field.valid_regexp,
407 :possible_values => field.possible_values.split('|'),
405 :possible_values => field.possible_values.split('|'),
408 :is_required => field.require_report?
406 :is_required => field.require_report?
409 next unless f.save
407 next unless f.save
410 print '.'
408 print '.'
411 STDOUT.flush
409 STDOUT.flush
412 # Trackers association
410 # Trackers association
413 f.trackers = Tracker.find :all
411 f.trackers = Tracker.find :all
414
412
415 # Projects association
413 # Projects association
416 field.projects.each do |project|
414 field.projects.each do |project|
417 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
415 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
418 end
416 end
419
417
420 # Values
418 # Values
421 field.values.each do |value|
419 field.values.each do |value|
422 v = CustomValue.new :custom_field_id => f.id,
420 v = CustomValue.new :custom_field_id => f.id,
423 :value => value.value
421 :value => value.value
424 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
422 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
425 v.save
423 v.save
426 end unless f.new_record?
424 end unless f.new_record?
427 end
425 end
428 puts
426 puts
429
427
430 puts
428 puts
431 puts "Users: #{users_migrated}/#{MantisUser.count}"
429 puts "Users: #{users_migrated}/#{MantisUser.count}"
432 puts "Projects: #{Project.count}/#{MantisProject.count}"
430 puts "Projects: #{Project.count}/#{MantisProject.count}"
433 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
431 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
434 puts "Versions: #{Version.count}/#{MantisVersion.count}"
432 puts "Versions: #{Version.count}/#{MantisVersion.count}"
435 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
433 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
436 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
434 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
437 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
435 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
438 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
436 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
439 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
437 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
440 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
438 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
441 puts "News: #{News.count}/#{MantisNews.count}"
439 puts "News: #{News.count}/#{MantisNews.count}"
442 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
440 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
443 end
441 end
444
442
445 def self.encoding(charset)
443 def self.encoding(charset)
446 @ic = Iconv.new('UTF-8', charset)
444 @ic = Iconv.new('UTF-8', charset)
447 rescue Iconv::InvalidEncoding
445 rescue Iconv::InvalidEncoding
448 return false
446 return false
449 end
447 end
450
448
451 def self.establish_connection(params)
449 def self.establish_connection(params)
452 constants.each do |const|
450 constants.each do |const|
453 klass = const_get(const)
451 klass = const_get(const)
454 next unless klass.respond_to? 'establish_connection'
452 next unless klass.respond_to? 'establish_connection'
455 klass.establish_connection params
453 klass.establish_connection params
456 end
454 end
457 end
455 end
458
456
459 def self.encode(text)
457 def self.encode(text)
460 @ic.iconv text
458 @ic.iconv text
461 rescue
459 rescue
462 text
460 text
463 end
461 end
464 end
462 end
465
463
466 puts
464 puts
467 if Redmine::DefaultData::Loader.no_data?
465 if Redmine::DefaultData::Loader.no_data?
468 puts "Redmine configuration need to be loaded before importing data."
466 puts "Redmine configuration need to be loaded before importing data."
469 puts "Please, run this first:"
467 puts "Please, run this first:"
470 puts
468 puts
471 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
472 exit
470 exit
473 end
471 end
474
472
475 puts "WARNING: Your Redmine data will be deleted during this process."
473 puts "WARNING: Your Redmine data will be deleted during this process."
476 print "Are you sure you want to continue ? [y/N] "
474 print "Are you sure you want to continue ? [y/N] "
477 STDOUT.flush
475 STDOUT.flush
478 break unless STDIN.gets.match(/^y$/i)
476 break unless STDIN.gets.match(/^y$/i)
479
477
480 # Default Mantis database settings
478 # Default Mantis database settings
481 db_params = {:adapter => 'mysql',
479 db_params = {:adapter => 'mysql',
482 :database => 'bugtracker',
480 :database => 'bugtracker',
483 :host => 'localhost',
481 :host => 'localhost',
484 :username => 'root',
482 :username => 'root',
485 :password => '' }
483 :password => '' }
486
484
487 puts
485 puts
488 puts "Please enter settings for your Mantis database"
486 puts "Please enter settings for your Mantis database"
489 [:adapter, :host, :database, :username, :password].each do |param|
487 [:adapter, :host, :database, :username, :password].each do |param|
490 print "#{param} [#{db_params[param]}]: "
488 print "#{param} [#{db_params[param]}]: "
491 value = STDIN.gets.chomp!
489 value = STDIN.gets.chomp!
492 db_params[param] = value unless value.blank?
490 db_params[param] = value unless value.blank?
493 end
491 end
494
492
495 while true
493 while true
496 print "encoding [UTF-8]: "
494 print "encoding [UTF-8]: "
497 STDOUT.flush
495 STDOUT.flush
498 encoding = STDIN.gets.chomp!
496 encoding = STDIN.gets.chomp!
499 encoding = 'UTF-8' if encoding.blank?
497 encoding = 'UTF-8' if encoding.blank?
500 break if MantisMigrate.encoding encoding
498 break if MantisMigrate.encoding encoding
501 puts "Invalid encoding!"
499 puts "Invalid encoding!"
502 end
500 end
503 puts
501 puts
504
502
505 # Make sure bugs can refer bugs in other projects
503 # Make sure bugs can refer bugs in other projects
506 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
504 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
507
505
508 # Turn off email notifications
506 # Turn off email notifications
509 Setting.notified_events = []
507 Setting.notified_events = []
510
508
511 MantisMigrate.establish_connection db_params
509 MantisMigrate.establish_connection db_params
512 MantisMigrate.migrate
510 MantisMigrate.migrate
513 end
511 end
514 end
512 end
@@ -1,768 +1,768
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'active_record'
18 require 'active_record'
19 require 'iconv'
19 require 'iconv'
20 require 'pp'
20 require 'pp'
21
21
22 namespace :redmine do
22 namespace :redmine do
23 desc 'Trac migration script'
23 desc 'Trac migration script'
24 task :migrate_from_trac => :environment do
24 task :migrate_from_trac => :environment do
25
25
26 module TracMigrate
26 module TracMigrate
27 TICKET_MAP = []
27 TICKET_MAP = []
28
28
29 DEFAULT_STATUS = IssueStatus.default
29 DEFAULT_STATUS = IssueStatus.default
30 assigned_status = IssueStatus.find_by_position(2)
30 assigned_status = IssueStatus.find_by_position(2)
31 resolved_status = IssueStatus.find_by_position(3)
31 resolved_status = IssueStatus.find_by_position(3)
32 feedback_status = IssueStatus.find_by_position(4)
32 feedback_status = IssueStatus.find_by_position(4)
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
33 closed_status = IssueStatus.find :first, :conditions => { :is_closed => true }
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
34 STATUS_MAPPING = {'new' => DEFAULT_STATUS,
35 'reopened' => feedback_status,
35 'reopened' => feedback_status,
36 'assigned' => assigned_status,
36 'assigned' => assigned_status,
37 'closed' => closed_status
37 'closed' => closed_status
38 }
38 }
39
39
40 priorities = IssuePriority.all
40 priorities = IssuePriority.all
41 DEFAULT_PRIORITY = priorities[0]
41 DEFAULT_PRIORITY = priorities[0]
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
42 PRIORITY_MAPPING = {'lowest' => priorities[0],
43 'low' => priorities[0],
43 'low' => priorities[0],
44 'normal' => priorities[1],
44 'normal' => priorities[1],
45 'high' => priorities[2],
45 'high' => priorities[2],
46 'highest' => priorities[3],
46 'highest' => priorities[3],
47 # ---
47 # ---
48 'trivial' => priorities[0],
48 'trivial' => priorities[0],
49 'minor' => priorities[1],
49 'minor' => priorities[1],
50 'major' => priorities[2],
50 'major' => priorities[2],
51 'critical' => priorities[3],
51 'critical' => priorities[3],
52 'blocker' => priorities[4]
52 'blocker' => priorities[4]
53 }
53 }
54
54
55 TRACKER_BUG = Tracker.find_by_position(1)
55 TRACKER_BUG = Tracker.find_by_position(1)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
56 TRACKER_FEATURE = Tracker.find_by_position(2)
57 DEFAULT_TRACKER = TRACKER_BUG
57 DEFAULT_TRACKER = TRACKER_BUG
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
58 TRACKER_MAPPING = {'defect' => TRACKER_BUG,
59 'enhancement' => TRACKER_FEATURE,
59 'enhancement' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE,
60 'task' => TRACKER_FEATURE,
61 'patch' =>TRACKER_FEATURE
61 'patch' =>TRACKER_FEATURE
62 }
62 }
63
63
64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
64 roles = Role.find(:all, :conditions => {:builtin => 0}, :order => 'position ASC')
65 manager_role = roles[0]
65 manager_role = roles[0]
66 developer_role = roles[1]
66 developer_role = roles[1]
67 DEFAULT_ROLE = roles.last
67 DEFAULT_ROLE = roles.last
68 ROLE_MAPPING = {'admin' => manager_role,
68 ROLE_MAPPING = {'admin' => manager_role,
69 'developer' => developer_role
69 'developer' => developer_role
70 }
70 }
71
71
72 class ::Time
72 class ::Time
73 class << self
73 class << self
74 alias :real_now :now
74 alias :real_now :now
75 def now
75 def now
76 real_now - @fake_diff.to_i
76 real_now - @fake_diff.to_i
77 end
77 end
78 def fake(time)
78 def fake(time)
79 @fake_diff = real_now - time
79 @fake_diff = real_now - time
80 res = yield
80 res = yield
81 @fake_diff = 0
81 @fake_diff = 0
82 res
82 res
83 end
83 end
84 end
84 end
85 end
85 end
86
86
87 class TracComponent < ActiveRecord::Base
87 class TracComponent < ActiveRecord::Base
88 set_table_name :component
88 set_table_name :component
89 end
89 end
90
90
91 class TracMilestone < ActiveRecord::Base
91 class TracMilestone < ActiveRecord::Base
92 set_table_name :milestone
92 set_table_name :milestone
93 # If this attribute is set a milestone has a defined target timepoint
93 # If this attribute is set a milestone has a defined target timepoint
94 def due
94 def due
95 if read_attribute(:due) && read_attribute(:due) > 0
95 if read_attribute(:due) && read_attribute(:due) > 0
96 Time.at(read_attribute(:due)).to_date
96 Time.at(read_attribute(:due)).to_date
97 else
97 else
98 nil
98 nil
99 end
99 end
100 end
100 end
101 # This is the real timepoint at which the milestone has finished.
101 # This is the real timepoint at which the milestone has finished.
102 def completed
102 def completed
103 if read_attribute(:completed) && read_attribute(:completed) > 0
103 if read_attribute(:completed) && read_attribute(:completed) > 0
104 Time.at(read_attribute(:completed)).to_date
104 Time.at(read_attribute(:completed)).to_date
105 else
105 else
106 nil
106 nil
107 end
107 end
108 end
108 end
109
109
110 def description
110 def description
111 # Attribute is named descr in Trac v0.8.x
111 # Attribute is named descr in Trac v0.8.x
112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
112 has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
113 end
113 end
114 end
114 end
115
115
116 class TracTicketCustom < ActiveRecord::Base
116 class TracTicketCustom < ActiveRecord::Base
117 set_table_name :ticket_custom
117 set_table_name :ticket_custom
118 end
118 end
119
119
120 class TracAttachment < ActiveRecord::Base
120 class TracAttachment < ActiveRecord::Base
121 set_table_name :attachment
121 set_table_name :attachment
122 set_inheritance_column :none
122 set_inheritance_column :none
123
123
124 def time; Time.at(read_attribute(:time)) end
124 def time; Time.at(read_attribute(:time)) end
125
125
126 def original_filename
126 def original_filename
127 filename
127 filename
128 end
128 end
129
129
130 def content_type
130 def content_type
131 ''
131 ''
132 end
132 end
133
133
134 def exist?
134 def exist?
135 File.file? trac_fullpath
135 File.file? trac_fullpath
136 end
136 end
137
137
138 def open
138 def open
139 File.open("#{trac_fullpath}", 'rb') {|f|
139 File.open("#{trac_fullpath}", 'rb') {|f|
140 @file = f
140 @file = f
141 yield self
141 yield self
142 }
142 }
143 end
143 end
144
144
145 def read(*args)
145 def read(*args)
146 @file.read(*args)
146 @file.read(*args)
147 end
147 end
148
148
149 def description
149 def description
150 read_attribute(:description).to_s.slice(0,255)
150 read_attribute(:description).to_s.slice(0,255)
151 end
151 end
152
152
153 private
153 private
154 def trac_fullpath
154 def trac_fullpath
155 attachment_type = read_attribute(:type)
155 attachment_type = read_attribute(:type)
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
156 trac_file = filename.gsub( /[^a-zA-Z0-9\-_\.!~*']/n ) {|x| sprintf('%%%02x', x[0]) }
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
157 "#{TracMigrate.trac_attachments_directory}/#{attachment_type}/#{id}/#{trac_file}"
158 end
158 end
159 end
159 end
160
160
161 class TracTicket < ActiveRecord::Base
161 class TracTicket < ActiveRecord::Base
162 set_table_name :ticket
162 set_table_name :ticket
163 set_inheritance_column :none
163 set_inheritance_column :none
164
164
165 # ticket changes: only migrate status changes and comments
165 # ticket changes: only migrate status changes and comments
166 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
166 has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 has_many :attachments, :class_name => "TracAttachment",
167 has_many :attachments, :class_name => "TracAttachment",
168 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
168 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
169 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
169 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
170 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
170 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
171 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
172
172
173 def ticket_type
173 def ticket_type
174 read_attribute(:type)
174 read_attribute(:type)
175 end
175 end
176
176
177 def summary
177 def summary
178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
178 read_attribute(:summary).blank? ? "(no subject)" : read_attribute(:summary)
179 end
179 end
180
180
181 def description
181 def description
182 read_attribute(:description).blank? ? summary : read_attribute(:description)
182 read_attribute(:description).blank? ? summary : read_attribute(:description)
183 end
183 end
184
184
185 def time; Time.at(read_attribute(:time)) end
185 def time; Time.at(read_attribute(:time)) end
186 def changetime; Time.at(read_attribute(:changetime)) end
186 def changetime; Time.at(read_attribute(:changetime)) end
187 end
187 end
188
188
189 class TracTicketChange < ActiveRecord::Base
189 class TracTicketChange < ActiveRecord::Base
190 set_table_name :ticket_change
190 set_table_name :ticket_change
191
191
192 def time; Time.at(read_attribute(:time)) end
192 def time; Time.at(read_attribute(:time)) end
193 end
193 end
194
194
195 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
195 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
196 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
196 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
197 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
197 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
198 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
198 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
199 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
199 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
200 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
200 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
201 CamelCase TitleIndex)
201 CamelCase TitleIndex)
202
202
203 class TracWikiPage < ActiveRecord::Base
203 class TracWikiPage < ActiveRecord::Base
204 set_table_name :wiki
204 set_table_name :wiki
205 set_primary_key :name
205 set_primary_key :name
206
206
207 has_many :attachments, :class_name => "TracAttachment",
207 has_many :attachments, :class_name => "TracAttachment",
208 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
208 :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
209 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
209 " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
210 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
210 ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{TracMigrate::TracAttachment.connection.quote_string(id.to_s)}\''
211
211
212 def self.columns
212 def self.columns
213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
213 # Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
214 super.select {|column| column.name.to_s != 'readonly'}
214 super.select {|column| column.name.to_s != 'readonly'}
215 end
215 end
216
216
217 def time; Time.at(read_attribute(:time)) end
217 def time; Time.at(read_attribute(:time)) end
218 end
218 end
219
219
220 class TracPermission < ActiveRecord::Base
220 class TracPermission < ActiveRecord::Base
221 set_table_name :permission
221 set_table_name :permission
222 end
222 end
223
223
224 class TracSessionAttribute < ActiveRecord::Base
224 class TracSessionAttribute < ActiveRecord::Base
225 set_table_name :session_attribute
225 set_table_name :session_attribute
226 end
226 end
227
227
228 def self.find_or_create_user(username, project_member = false)
228 def self.find_or_create_user(username, project_member = false)
229 return User.anonymous if username.blank?
229 return User.anonymous if username.blank?
230
230
231 u = User.find_by_login(username)
231 u = User.find_by_login(username)
232 if !u
232 if !u
233 # Create a new user if not found
233 # Create a new user if not found
234 mail = username[0,limit_for(User, 'mail')]
234 mail = username[0,limit_for(User, 'mail')]
235 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
235 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
236 mail = mail_attr.value
236 mail = mail_attr.value
237 end
237 end
238 mail = "#{mail}@foo.bar" unless mail.include?("@")
238 mail = "#{mail}@foo.bar" unless mail.include?("@")
239
239
240 name = username
240 name = username
241 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
241 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
242 name = name_attr.value
242 name = name_attr.value
243 end
243 end
244 name =~ (/(.*)(\s+\w+)?/)
244 name =~ (/(.*)(\s+\w+)?/)
245 fn = $1.strip
245 fn = $1.strip
246 ln = ($2 || '-').strip
246 ln = ($2 || '-').strip
247
247
248 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
248 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
249 :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
249 :firstname => fn[0, limit_for(User, 'firstname')],
250 :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
250 :lastname => ln[0, limit_for(User, 'lastname')]
251
251
252 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
252 u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
253 u.password = 'trac'
253 u.password = 'trac'
254 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
254 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
255 # finally, a default user is used if the new user is not valid
255 # finally, a default user is used if the new user is not valid
256 u = User.find(:first) unless u.save
256 u = User.find(:first) unless u.save
257 end
257 end
258 # Make sure he is a member of the project
258 # Make sure he is a member of the project
259 if project_member && !u.member_of?(@target_project)
259 if project_member && !u.member_of?(@target_project)
260 role = DEFAULT_ROLE
260 role = DEFAULT_ROLE
261 if u.admin
261 if u.admin
262 role = ROLE_MAPPING['admin']
262 role = ROLE_MAPPING['admin']
263 elsif TracPermission.find_by_username_and_action(username, 'developer')
263 elsif TracPermission.find_by_username_and_action(username, 'developer')
264 role = ROLE_MAPPING['developer']
264 role = ROLE_MAPPING['developer']
265 end
265 end
266 Member.create(:user => u, :project => @target_project, :roles => [role])
266 Member.create(:user => u, :project => @target_project, :roles => [role])
267 u.reload
267 u.reload
268 end
268 end
269 u
269 u
270 end
270 end
271
271
272 # Basic wiki syntax conversion
272 # Basic wiki syntax conversion
273 def self.convert_wiki_text(text)
273 def self.convert_wiki_text(text)
274 # Titles
274 # Titles
275 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
275 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
276 # External Links
276 # External Links
277 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
277 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
278 # Ticket links:
278 # Ticket links:
279 # [ticket:234 Text],[ticket:234 This is a test]
279 # [ticket:234 Text],[ticket:234 This is a test]
280 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
280 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
281 # ticket:1234
281 # ticket:1234
282 # #1 is working cause Redmine uses the same syntax.
282 # #1 is working cause Redmine uses the same syntax.
283 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
283 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
284 # Milestone links:
284 # Milestone links:
285 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
285 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
286 # The text "Milestone 0.1.0 (Mercury)" is not converted,
286 # The text "Milestone 0.1.0 (Mercury)" is not converted,
287 # cause Redmine's wiki does not support this.
287 # cause Redmine's wiki does not support this.
288 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
288 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
289 # [milestone:"0.1.0 Mercury"]
289 # [milestone:"0.1.0 Mercury"]
290 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
290 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
291 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
291 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
292 # milestone:0.1.0
292 # milestone:0.1.0
293 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
293 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
294 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
294 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
295 # Internal Links
295 # Internal Links
296 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
296 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
297 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
297 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
298 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
298 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
299 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
299 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
300 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
300 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
301 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
302
302
303 # Links to pages UsingJustWikiCaps
303 # Links to pages UsingJustWikiCaps
304 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
304 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
305 # Normalize things that were supposed to not be links
305 # Normalize things that were supposed to not be links
306 # like !NotALink
306 # like !NotALink
307 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
307 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
308 # Revisions links
308 # Revisions links
309 text = text.gsub(/\[(\d+)\]/, 'r\1')
309 text = text.gsub(/\[(\d+)\]/, 'r\1')
310 # Ticket number re-writing
310 # Ticket number re-writing
311 text = text.gsub(/#(\d+)/) do |s|
311 text = text.gsub(/#(\d+)/) do |s|
312 if $1.length < 10
312 if $1.length < 10
313 # TICKET_MAP[$1.to_i] ||= $1
313 # TICKET_MAP[$1.to_i] ||= $1
314 "\##{TICKET_MAP[$1.to_i] || $1}"
314 "\##{TICKET_MAP[$1.to_i] || $1}"
315 else
315 else
316 s
316 s
317 end
317 end
318 end
318 end
319 # We would like to convert the Code highlighting too
319 # We would like to convert the Code highlighting too
320 # This will go into the next line.
320 # This will go into the next line.
321 shebang_line = false
321 shebang_line = false
322 # Reguar expression for start of code
322 # Reguar expression for start of code
323 pre_re = /\{\{\{/
323 pre_re = /\{\{\{/
324 # Code hightlighing...
324 # Code hightlighing...
325 shebang_re = /^\#\!([a-z]+)/
325 shebang_re = /^\#\!([a-z]+)/
326 # Regular expression for end of code
326 # Regular expression for end of code
327 pre_end_re = /\}\}\}/
327 pre_end_re = /\}\}\}/
328
328
329 # Go through the whole text..extract it line by line
329 # Go through the whole text..extract it line by line
330 text = text.gsub(/^(.*)$/) do |line|
330 text = text.gsub(/^(.*)$/) do |line|
331 m_pre = pre_re.match(line)
331 m_pre = pre_re.match(line)
332 if m_pre
332 if m_pre
333 line = '<pre>'
333 line = '<pre>'
334 else
334 else
335 m_sl = shebang_re.match(line)
335 m_sl = shebang_re.match(line)
336 if m_sl
336 if m_sl
337 shebang_line = true
337 shebang_line = true
338 line = '<code class="' + m_sl[1] + '">'
338 line = '<code class="' + m_sl[1] + '">'
339 end
339 end
340 m_pre_end = pre_end_re.match(line)
340 m_pre_end = pre_end_re.match(line)
341 if m_pre_end
341 if m_pre_end
342 line = '</pre>'
342 line = '</pre>'
343 if shebang_line
343 if shebang_line
344 line = '</code>' + line
344 line = '</code>' + line
345 end
345 end
346 end
346 end
347 end
347 end
348 line
348 line
349 end
349 end
350
350
351 # Highlighting
351 # Highlighting
352 text = text.gsub(/'''''([^\s])/, '_*\1')
352 text = text.gsub(/'''''([^\s])/, '_*\1')
353 text = text.gsub(/([^\s])'''''/, '\1*_')
353 text = text.gsub(/([^\s])'''''/, '\1*_')
354 text = text.gsub(/'''/, '*')
354 text = text.gsub(/'''/, '*')
355 text = text.gsub(/''/, '_')
355 text = text.gsub(/''/, '_')
356 text = text.gsub(/__/, '+')
356 text = text.gsub(/__/, '+')
357 text = text.gsub(/~~/, '-')
357 text = text.gsub(/~~/, '-')
358 text = text.gsub(/`/, '@')
358 text = text.gsub(/`/, '@')
359 text = text.gsub(/,,/, '~')
359 text = text.gsub(/,,/, '~')
360 # Lists
360 # Lists
361 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
361 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
362
362
363 text
363 text
364 end
364 end
365
365
366 def self.migrate
366 def self.migrate
367 establish_connection
367 establish_connection
368
368
369 # Quick database test
369 # Quick database test
370 TracComponent.count
370 TracComponent.count
371
371
372 migrated_components = 0
372 migrated_components = 0
373 migrated_milestones = 0
373 migrated_milestones = 0
374 migrated_tickets = 0
374 migrated_tickets = 0
375 migrated_custom_values = 0
375 migrated_custom_values = 0
376 migrated_ticket_attachments = 0
376 migrated_ticket_attachments = 0
377 migrated_wiki_edits = 0
377 migrated_wiki_edits = 0
378 migrated_wiki_attachments = 0
378 migrated_wiki_attachments = 0
379
379
380 #Wiki system initializing...
380 #Wiki system initializing...
381 @target_project.wiki.destroy if @target_project.wiki
381 @target_project.wiki.destroy if @target_project.wiki
382 @target_project.reload
382 @target_project.reload
383 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
383 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
384 wiki_edit_count = 0
384 wiki_edit_count = 0
385
385
386 # Components
386 # Components
387 print "Migrating components"
387 print "Migrating components"
388 issues_category_map = {}
388 issues_category_map = {}
389 TracComponent.find(:all).each do |component|
389 TracComponent.find(:all).each do |component|
390 print '.'
390 print '.'
391 STDOUT.flush
391 STDOUT.flush
392 c = IssueCategory.new :project => @target_project,
392 c = IssueCategory.new :project => @target_project,
393 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
393 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
394 next unless c.save
394 next unless c.save
395 issues_category_map[component.name] = c
395 issues_category_map[component.name] = c
396 migrated_components += 1
396 migrated_components += 1
397 end
397 end
398 puts
398 puts
399
399
400 # Milestones
400 # Milestones
401 print "Migrating milestones"
401 print "Migrating milestones"
402 version_map = {}
402 version_map = {}
403 TracMilestone.find(:all).each do |milestone|
403 TracMilestone.find(:all).each do |milestone|
404 print '.'
404 print '.'
405 STDOUT.flush
405 STDOUT.flush
406 # First we try to find the wiki page...
406 # First we try to find the wiki page...
407 p = wiki.find_or_new_page(milestone.name.to_s)
407 p = wiki.find_or_new_page(milestone.name.to_s)
408 p.content = WikiContent.new(:page => p) if p.new_record?
408 p.content = WikiContent.new(:page => p) if p.new_record?
409 p.content.text = milestone.description.to_s
409 p.content.text = milestone.description.to_s
410 p.content.author = find_or_create_user('trac')
410 p.content.author = find_or_create_user('trac')
411 p.content.comments = 'Milestone'
411 p.content.comments = 'Milestone'
412 p.save
412 p.save
413
413
414 v = Version.new :project => @target_project,
414 v = Version.new :project => @target_project,
415 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
415 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
416 :description => nil,
416 :description => nil,
417 :wiki_page_title => milestone.name.to_s,
417 :wiki_page_title => milestone.name.to_s,
418 :effective_date => milestone.completed
418 :effective_date => milestone.completed
419
419
420 next unless v.save
420 next unless v.save
421 version_map[milestone.name] = v
421 version_map[milestone.name] = v
422 migrated_milestones += 1
422 migrated_milestones += 1
423 end
423 end
424 puts
424 puts
425
425
426 # Custom fields
426 # Custom fields
427 # TODO: read trac.ini instead
427 # TODO: read trac.ini instead
428 print "Migrating custom fields"
428 print "Migrating custom fields"
429 custom_field_map = {}
429 custom_field_map = {}
430 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
430 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
431 print '.'
431 print '.'
432 STDOUT.flush
432 STDOUT.flush
433 # Redmine custom field name
433 # Redmine custom field name
434 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
434 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
435 # Find if the custom already exists in Redmine
435 # Find if the custom already exists in Redmine
436 f = IssueCustomField.find_by_name(field_name)
436 f = IssueCustomField.find_by_name(field_name)
437 # Or create a new one
437 # Or create a new one
438 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
438 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
439 :field_format => 'string')
439 :field_format => 'string')
440
440
441 next if f.new_record?
441 next if f.new_record?
442 f.trackers = Tracker.find(:all)
442 f.trackers = Tracker.find(:all)
443 f.projects << @target_project
443 f.projects << @target_project
444 custom_field_map[field.name] = f
444 custom_field_map[field.name] = f
445 end
445 end
446 puts
446 puts
447
447
448 # Trac 'resolution' field as a Redmine custom field
448 # Trac 'resolution' field as a Redmine custom field
449 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
449 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
450 r = IssueCustomField.new(:name => 'Resolution',
450 r = IssueCustomField.new(:name => 'Resolution',
451 :field_format => 'list',
451 :field_format => 'list',
452 :is_filter => true) if r.nil?
452 :is_filter => true) if r.nil?
453 r.trackers = Tracker.find(:all)
453 r.trackers = Tracker.find(:all)
454 r.projects << @target_project
454 r.projects << @target_project
455 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
455 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
456 r.save!
456 r.save!
457 custom_field_map['resolution'] = r
457 custom_field_map['resolution'] = r
458
458
459 # Tickets
459 # Tickets
460 print "Migrating tickets"
460 print "Migrating tickets"
461 TracTicket.find_each(:batch_size => 200) do |ticket|
461 TracTicket.find_each(:batch_size => 200) do |ticket|
462 print '.'
462 print '.'
463 STDOUT.flush
463 STDOUT.flush
464 i = Issue.new :project => @target_project,
464 i = Issue.new :project => @target_project,
465 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
465 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
466 :description => convert_wiki_text(encode(ticket.description)),
466 :description => convert_wiki_text(encode(ticket.description)),
467 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
467 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
468 :created_on => ticket.time
468 :created_on => ticket.time
469 i.author = find_or_create_user(ticket.reporter)
469 i.author = find_or_create_user(ticket.reporter)
470 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
470 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
471 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
471 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
472 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
472 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
473 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
473 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
474 i.id = ticket.id unless Issue.exists?(ticket.id)
474 i.id = ticket.id unless Issue.exists?(ticket.id)
475 next unless Time.fake(ticket.changetime) { i.save }
475 next unless Time.fake(ticket.changetime) { i.save }
476 TICKET_MAP[ticket.id] = i.id
476 TICKET_MAP[ticket.id] = i.id
477 migrated_tickets += 1
477 migrated_tickets += 1
478
478
479 # Owner
479 # Owner
480 unless ticket.owner.blank?
480 unless ticket.owner.blank?
481 i.assigned_to = find_or_create_user(ticket.owner, true)
481 i.assigned_to = find_or_create_user(ticket.owner, true)
482 Time.fake(ticket.changetime) { i.save }
482 Time.fake(ticket.changetime) { i.save }
483 end
483 end
484
484
485 # Comments and status/resolution changes
485 # Comments and status/resolution changes
486 ticket.changes.group_by(&:time).each do |time, changeset|
486 ticket.changes.group_by(&:time).each do |time, changeset|
487 status_change = changeset.select {|change| change.field == 'status'}.first
487 status_change = changeset.select {|change| change.field == 'status'}.first
488 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
488 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
489 comment_change = changeset.select {|change| change.field == 'comment'}.first
489 comment_change = changeset.select {|change| change.field == 'comment'}.first
490
490
491 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
491 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
492 :created_on => time
492 :created_on => time
493 n.user = find_or_create_user(changeset.first.author)
493 n.user = find_or_create_user(changeset.first.author)
494 n.journalized = i
494 n.journalized = i
495 if status_change &&
495 if status_change &&
496 STATUS_MAPPING[status_change.oldvalue] &&
496 STATUS_MAPPING[status_change.oldvalue] &&
497 STATUS_MAPPING[status_change.newvalue] &&
497 STATUS_MAPPING[status_change.newvalue] &&
498 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
498 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
499 n.details << JournalDetail.new(:property => 'attr',
499 n.details << JournalDetail.new(:property => 'attr',
500 :prop_key => 'status_id',
500 :prop_key => 'status_id',
501 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
501 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
502 :value => STATUS_MAPPING[status_change.newvalue].id)
502 :value => STATUS_MAPPING[status_change.newvalue].id)
503 end
503 end
504 if resolution_change
504 if resolution_change
505 n.details << JournalDetail.new(:property => 'cf',
505 n.details << JournalDetail.new(:property => 'cf',
506 :prop_key => custom_field_map['resolution'].id,
506 :prop_key => custom_field_map['resolution'].id,
507 :old_value => resolution_change.oldvalue,
507 :old_value => resolution_change.oldvalue,
508 :value => resolution_change.newvalue)
508 :value => resolution_change.newvalue)
509 end
509 end
510 n.save unless n.details.empty? && n.notes.blank?
510 n.save unless n.details.empty? && n.notes.blank?
511 end
511 end
512
512
513 # Attachments
513 # Attachments
514 ticket.attachments.each do |attachment|
514 ticket.attachments.each do |attachment|
515 next unless attachment.exist?
515 next unless attachment.exist?
516 attachment.open {
516 attachment.open {
517 a = Attachment.new :created_on => attachment.time
517 a = Attachment.new :created_on => attachment.time
518 a.file = attachment
518 a.file = attachment
519 a.author = find_or_create_user(attachment.author)
519 a.author = find_or_create_user(attachment.author)
520 a.container = i
520 a.container = i
521 a.description = attachment.description
521 a.description = attachment.description
522 migrated_ticket_attachments += 1 if a.save
522 migrated_ticket_attachments += 1 if a.save
523 }
523 }
524 end
524 end
525
525
526 # Custom fields
526 # Custom fields
527 custom_values = ticket.customs.inject({}) do |h, custom|
527 custom_values = ticket.customs.inject({}) do |h, custom|
528 if custom_field = custom_field_map[custom.name]
528 if custom_field = custom_field_map[custom.name]
529 h[custom_field.id] = custom.value
529 h[custom_field.id] = custom.value
530 migrated_custom_values += 1
530 migrated_custom_values += 1
531 end
531 end
532 h
532 h
533 end
533 end
534 if custom_field_map['resolution'] && !ticket.resolution.blank?
534 if custom_field_map['resolution'] && !ticket.resolution.blank?
535 custom_values[custom_field_map['resolution'].id] = ticket.resolution
535 custom_values[custom_field_map['resolution'].id] = ticket.resolution
536 end
536 end
537 i.custom_field_values = custom_values
537 i.custom_field_values = custom_values
538 i.save_custom_field_values
538 i.save_custom_field_values
539 end
539 end
540
540
541 # update issue id sequence if needed (postgresql)
541 # update issue id sequence if needed (postgresql)
542 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
542 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
543 puts
543 puts
544
544
545 # Wiki
545 # Wiki
546 print "Migrating wiki"
546 print "Migrating wiki"
547 if wiki.save
547 if wiki.save
548 TracWikiPage.find(:all, :order => 'name, version').each do |page|
548 TracWikiPage.find(:all, :order => 'name, version').each do |page|
549 # Do not migrate Trac manual wiki pages
549 # Do not migrate Trac manual wiki pages
550 next if TRAC_WIKI_PAGES.include?(page.name)
550 next if TRAC_WIKI_PAGES.include?(page.name)
551 wiki_edit_count += 1
551 wiki_edit_count += 1
552 print '.'
552 print '.'
553 STDOUT.flush
553 STDOUT.flush
554 p = wiki.find_or_new_page(page.name)
554 p = wiki.find_or_new_page(page.name)
555 p.content = WikiContent.new(:page => p) if p.new_record?
555 p.content = WikiContent.new(:page => p) if p.new_record?
556 p.content.text = page.text
556 p.content.text = page.text
557 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
557 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
558 p.content.comments = page.comment
558 p.content.comments = page.comment
559 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
559 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
560
560
561 next if p.content.new_record?
561 next if p.content.new_record?
562 migrated_wiki_edits += 1
562 migrated_wiki_edits += 1
563
563
564 # Attachments
564 # Attachments
565 page.attachments.each do |attachment|
565 page.attachments.each do |attachment|
566 next unless attachment.exist?
566 next unless attachment.exist?
567 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
567 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
568 attachment.open {
568 attachment.open {
569 a = Attachment.new :created_on => attachment.time
569 a = Attachment.new :created_on => attachment.time
570 a.file = attachment
570 a.file = attachment
571 a.author = find_or_create_user(attachment.author)
571 a.author = find_or_create_user(attachment.author)
572 a.description = attachment.description
572 a.description = attachment.description
573 a.container = p
573 a.container = p
574 migrated_wiki_attachments += 1 if a.save
574 migrated_wiki_attachments += 1 if a.save
575 }
575 }
576 end
576 end
577 end
577 end
578
578
579 wiki.reload
579 wiki.reload
580 wiki.pages.each do |page|
580 wiki.pages.each do |page|
581 page.content.text = convert_wiki_text(page.content.text)
581 page.content.text = convert_wiki_text(page.content.text)
582 Time.fake(page.content.updated_on) { page.content.save }
582 Time.fake(page.content.updated_on) { page.content.save }
583 end
583 end
584 end
584 end
585 puts
585 puts
586
586
587 puts
587 puts
588 puts "Components: #{migrated_components}/#{TracComponent.count}"
588 puts "Components: #{migrated_components}/#{TracComponent.count}"
589 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
589 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
590 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
590 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
591 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
591 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
592 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
592 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
593 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
593 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
594 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
594 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
595 end
595 end
596
596
597 def self.limit_for(klass, attribute)
597 def self.limit_for(klass, attribute)
598 klass.columns_hash[attribute.to_s].limit
598 klass.columns_hash[attribute.to_s].limit
599 end
599 end
600
600
601 def self.encoding(charset)
601 def self.encoding(charset)
602 @ic = Iconv.new('UTF-8', charset)
602 @ic = Iconv.new('UTF-8', charset)
603 rescue Iconv::InvalidEncoding
603 rescue Iconv::InvalidEncoding
604 puts "Invalid encoding!"
604 puts "Invalid encoding!"
605 return false
605 return false
606 end
606 end
607
607
608 def self.set_trac_directory(path)
608 def self.set_trac_directory(path)
609 @@trac_directory = path
609 @@trac_directory = path
610 raise "This directory doesn't exist!" unless File.directory?(path)
610 raise "This directory doesn't exist!" unless File.directory?(path)
611 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
611 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
612 @@trac_directory
612 @@trac_directory
613 rescue Exception => e
613 rescue Exception => e
614 puts e
614 puts e
615 return false
615 return false
616 end
616 end
617
617
618 def self.trac_directory
618 def self.trac_directory
619 @@trac_directory
619 @@trac_directory
620 end
620 end
621
621
622 def self.set_trac_adapter(adapter)
622 def self.set_trac_adapter(adapter)
623 return false if adapter.blank?
623 return false if adapter.blank?
624 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
624 raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
625 # If adapter is sqlite or sqlite3, make sure that trac.db exists
625 # If adapter is sqlite or sqlite3, make sure that trac.db exists
626 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
626 raise "#{trac_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(trac_db_path)
627 @@trac_adapter = adapter
627 @@trac_adapter = adapter
628 rescue Exception => e
628 rescue Exception => e
629 puts e
629 puts e
630 return false
630 return false
631 end
631 end
632
632
633 def self.set_trac_db_host(host)
633 def self.set_trac_db_host(host)
634 return nil if host.blank?
634 return nil if host.blank?
635 @@trac_db_host = host
635 @@trac_db_host = host
636 end
636 end
637
637
638 def self.set_trac_db_port(port)
638 def self.set_trac_db_port(port)
639 return nil if port.to_i == 0
639 return nil if port.to_i == 0
640 @@trac_db_port = port.to_i
640 @@trac_db_port = port.to_i
641 end
641 end
642
642
643 def self.set_trac_db_name(name)
643 def self.set_trac_db_name(name)
644 return nil if name.blank?
644 return nil if name.blank?
645 @@trac_db_name = name
645 @@trac_db_name = name
646 end
646 end
647
647
648 def self.set_trac_db_username(username)
648 def self.set_trac_db_username(username)
649 @@trac_db_username = username
649 @@trac_db_username = username
650 end
650 end
651
651
652 def self.set_trac_db_password(password)
652 def self.set_trac_db_password(password)
653 @@trac_db_password = password
653 @@trac_db_password = password
654 end
654 end
655
655
656 def self.set_trac_db_schema(schema)
656 def self.set_trac_db_schema(schema)
657 @@trac_db_schema = schema
657 @@trac_db_schema = schema
658 end
658 end
659
659
660 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
660 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
661
661
662 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
662 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
663 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
663 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
664
664
665 def self.target_project_identifier(identifier)
665 def self.target_project_identifier(identifier)
666 project = Project.find_by_identifier(identifier)
666 project = Project.find_by_identifier(identifier)
667 if !project
667 if !project
668 # create the target project
668 # create the target project
669 project = Project.new :name => identifier.humanize,
669 project = Project.new :name => identifier.humanize,
670 :description => ''
670 :description => ''
671 project.identifier = identifier
671 project.identifier = identifier
672 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
672 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
673 # enable issues and wiki for the created project
673 # enable issues and wiki for the created project
674 project.enabled_module_names = ['issue_tracking', 'wiki']
674 project.enabled_module_names = ['issue_tracking', 'wiki']
675 else
675 else
676 puts
676 puts
677 puts "This project already exists in your Redmine database."
677 puts "This project already exists in your Redmine database."
678 print "Are you sure you want to append data to this project ? [Y/n] "
678 print "Are you sure you want to append data to this project ? [Y/n] "
679 STDOUT.flush
679 STDOUT.flush
680 exit if STDIN.gets.match(/^n$/i)
680 exit if STDIN.gets.match(/^n$/i)
681 end
681 end
682 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
682 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
683 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
683 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
684 @target_project = project.new_record? ? nil : project
684 @target_project = project.new_record? ? nil : project
685 @target_project.reload
685 @target_project.reload
686 end
686 end
687
687
688 def self.connection_params
688 def self.connection_params
689 if %w(sqlite sqlite3).include?(trac_adapter)
689 if %w(sqlite sqlite3).include?(trac_adapter)
690 {:adapter => trac_adapter,
690 {:adapter => trac_adapter,
691 :database => trac_db_path}
691 :database => trac_db_path}
692 else
692 else
693 {:adapter => trac_adapter,
693 {:adapter => trac_adapter,
694 :database => trac_db_name,
694 :database => trac_db_name,
695 :host => trac_db_host,
695 :host => trac_db_host,
696 :port => trac_db_port,
696 :port => trac_db_port,
697 :username => trac_db_username,
697 :username => trac_db_username,
698 :password => trac_db_password,
698 :password => trac_db_password,
699 :schema_search_path => trac_db_schema
699 :schema_search_path => trac_db_schema
700 }
700 }
701 end
701 end
702 end
702 end
703
703
704 def self.establish_connection
704 def self.establish_connection
705 constants.each do |const|
705 constants.each do |const|
706 klass = const_get(const)
706 klass = const_get(const)
707 next unless klass.respond_to? 'establish_connection'
707 next unless klass.respond_to? 'establish_connection'
708 klass.establish_connection connection_params
708 klass.establish_connection connection_params
709 end
709 end
710 end
710 end
711
711
712 private
712 private
713 def self.encode(text)
713 def self.encode(text)
714 @ic.iconv text
714 @ic.iconv text
715 rescue
715 rescue
716 text
716 text
717 end
717 end
718 end
718 end
719
719
720 puts
720 puts
721 if Redmine::DefaultData::Loader.no_data?
721 if Redmine::DefaultData::Loader.no_data?
722 puts "Redmine configuration need to be loaded before importing data."
722 puts "Redmine configuration need to be loaded before importing data."
723 puts "Please, run this first:"
723 puts "Please, run this first:"
724 puts
724 puts
725 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
725 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
726 exit
726 exit
727 end
727 end
728
728
729 puts "WARNING: a new project will be added to Redmine during this process."
729 puts "WARNING: a new project will be added to Redmine during this process."
730 print "Are you sure you want to continue ? [y/N] "
730 print "Are you sure you want to continue ? [y/N] "
731 STDOUT.flush
731 STDOUT.flush
732 break unless STDIN.gets.match(/^y$/i)
732 break unless STDIN.gets.match(/^y$/i)
733 puts
733 puts
734
734
735 def prompt(text, options = {}, &block)
735 def prompt(text, options = {}, &block)
736 default = options[:default] || ''
736 default = options[:default] || ''
737 while true
737 while true
738 print "#{text} [#{default}]: "
738 print "#{text} [#{default}]: "
739 STDOUT.flush
739 STDOUT.flush
740 value = STDIN.gets.chomp!
740 value = STDIN.gets.chomp!
741 value = default if value.blank?
741 value = default if value.blank?
742 break if yield value
742 break if yield value
743 end
743 end
744 end
744 end
745
745
746 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
746 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
747
747
748 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
748 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
749 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
749 prompt('Trac database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'sqlite') {|adapter| TracMigrate.set_trac_adapter adapter}
750 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
750 unless %w(sqlite sqlite3).include?(TracMigrate.trac_adapter)
751 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
751 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
752 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
752 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
753 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
753 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
754 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
754 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
755 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
755 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
756 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
756 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
757 end
757 end
758 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
758 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
759 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
759 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
760 puts
760 puts
761
761
762 # Turn off email notifications
762 # Turn off email notifications
763 Setting.notified_events = []
763 Setting.notified_events = []
764
764
765 TracMigrate.migrate
765 TracMigrate.migrate
766 end
766 end
767 end
767 end
768
768
General Comments 0
You need to be logged in to leave comments. Login now