##// END OF EJS Templates
Replaces find(:all) calls....
Jean-Philippe Lang -
r10690:536747b74708
parent child
Show More
@@ -1,322 +1,322
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :custom_values, :dependent => :delete_all
21 has_many :custom_values, :dependent => :delete_all
22 acts_as_list :scope => 'type = \'#{self.class}\''
22 acts_as_list :scope => 'type = \'#{self.class}\''
23 serialize :possible_values
23 serialize :possible_values
24
24
25 validates_presence_of :name, :field_format
25 validates_presence_of :name, :field_format
26 validates_uniqueness_of :name, :scope => :type
26 validates_uniqueness_of :name, :scope => :type
27 validates_length_of :name, :maximum => 30
27 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29
29
30 validate :validate_custom_field
30 validate :validate_custom_field
31 before_validation :set_searchable
31 before_validation :set_searchable
32
32
33 scope :sorted, order("#{table_name}.position ASC")
33 scope :sorted, order("#{table_name}.position ASC")
34
34
35 CUSTOM_FIELDS_TABS = [
35 CUSTOM_FIELDS_TABS = [
36 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
36 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
37 :label => :label_issue_plural},
37 :label => :label_issue_plural},
38 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
38 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
39 :label => :label_spent_time},
39 :label => :label_spent_time},
40 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
40 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
41 :label => :label_project_plural},
41 :label => :label_project_plural},
42 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
42 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
43 :label => :label_version_plural},
43 :label => :label_version_plural},
44 {:name => 'UserCustomField', :partial => 'custom_fields/index',
44 {:name => 'UserCustomField', :partial => 'custom_fields/index',
45 :label => :label_user_plural},
45 :label => :label_user_plural},
46 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
46 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
47 :label => :label_group_plural},
47 :label => :label_group_plural},
48 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
48 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
49 :label => TimeEntryActivity::OptionName},
49 :label => TimeEntryActivity::OptionName},
50 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
50 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
51 :label => IssuePriority::OptionName},
51 :label => IssuePriority::OptionName},
52 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
52 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
53 :label => DocumentCategory::OptionName}
53 :label => DocumentCategory::OptionName}
54 ]
54 ]
55
55
56 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
56 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]}
57
57
58 def field_format=(arg)
58 def field_format=(arg)
59 # cannot change format of a saved custom field
59 # cannot change format of a saved custom field
60 super if new_record?
60 super if new_record?
61 end
61 end
62
62
63 def set_searchable
63 def set_searchable
64 # make sure these fields are not searchable
64 # make sure these fields are not searchable
65 self.searchable = false if %w(int float date bool).include?(field_format)
65 self.searchable = false if %w(int float date bool).include?(field_format)
66 # make sure only these fields can have multiple values
66 # make sure only these fields can have multiple values
67 self.multiple = false unless %w(list user version).include?(field_format)
67 self.multiple = false unless %w(list user version).include?(field_format)
68 true
68 true
69 end
69 end
70
70
71 def validate_custom_field
71 def validate_custom_field
72 if self.field_format == "list"
72 if self.field_format == "list"
73 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
73 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
74 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
74 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
75 end
75 end
76
76
77 if regexp.present?
77 if regexp.present?
78 begin
78 begin
79 Regexp.new(regexp)
79 Regexp.new(regexp)
80 rescue
80 rescue
81 errors.add(:regexp, :invalid)
81 errors.add(:regexp, :invalid)
82 end
82 end
83 end
83 end
84
84
85 if default_value.present? && !valid_field_value?(default_value)
85 if default_value.present? && !valid_field_value?(default_value)
86 errors.add(:default_value, :invalid)
86 errors.add(:default_value, :invalid)
87 end
87 end
88 end
88 end
89
89
90 def possible_values_options(obj=nil)
90 def possible_values_options(obj=nil)
91 case field_format
91 case field_format
92 when 'user', 'version'
92 when 'user', 'version'
93 if obj.respond_to?(:project) && obj.project
93 if obj.respond_to?(:project) && obj.project
94 case field_format
94 case field_format
95 when 'user'
95 when 'user'
96 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
96 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
97 when 'version'
97 when 'version'
98 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
98 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
99 end
99 end
100 elsif obj.is_a?(Array)
100 elsif obj.is_a?(Array)
101 obj.collect {|o| possible_values_options(o)}.reduce(:&)
101 obj.collect {|o| possible_values_options(o)}.reduce(:&)
102 else
102 else
103 []
103 []
104 end
104 end
105 when 'bool'
105 when 'bool'
106 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
106 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
107 else
107 else
108 possible_values || []
108 possible_values || []
109 end
109 end
110 end
110 end
111
111
112 def possible_values(obj=nil)
112 def possible_values(obj=nil)
113 case field_format
113 case field_format
114 when 'user', 'version'
114 when 'user', 'version'
115 possible_values_options(obj).collect(&:last)
115 possible_values_options(obj).collect(&:last)
116 when 'bool'
116 when 'bool'
117 ['1', '0']
117 ['1', '0']
118 else
118 else
119 values = super()
119 values = super()
120 if values.is_a?(Array)
120 if values.is_a?(Array)
121 values.each do |value|
121 values.each do |value|
122 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
122 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
123 end
123 end
124 end
124 end
125 values || []
125 values || []
126 end
126 end
127 end
127 end
128
128
129 # Makes possible_values accept a multiline string
129 # Makes possible_values accept a multiline string
130 def possible_values=(arg)
130 def possible_values=(arg)
131 if arg.is_a?(Array)
131 if arg.is_a?(Array)
132 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
132 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
133 else
133 else
134 self.possible_values = arg.to_s.split(/[\n\r]+/)
134 self.possible_values = arg.to_s.split(/[\n\r]+/)
135 end
135 end
136 end
136 end
137
137
138 def cast_value(value)
138 def cast_value(value)
139 casted = nil
139 casted = nil
140 unless value.blank?
140 unless value.blank?
141 case field_format
141 case field_format
142 when 'string', 'text', 'list'
142 when 'string', 'text', 'list'
143 casted = value
143 casted = value
144 when 'date'
144 when 'date'
145 casted = begin; value.to_date; rescue; nil end
145 casted = begin; value.to_date; rescue; nil end
146 when 'bool'
146 when 'bool'
147 casted = (value == '1' ? true : false)
147 casted = (value == '1' ? true : false)
148 when 'int'
148 when 'int'
149 casted = value.to_i
149 casted = value.to_i
150 when 'float'
150 when 'float'
151 casted = value.to_f
151 casted = value.to_f
152 when 'user', 'version'
152 when 'user', 'version'
153 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
153 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
154 end
154 end
155 end
155 end
156 casted
156 casted
157 end
157 end
158
158
159 def value_from_keyword(keyword, customized)
159 def value_from_keyword(keyword, customized)
160 possible_values_options = possible_values_options(customized)
160 possible_values_options = possible_values_options(customized)
161 if possible_values_options.present?
161 if possible_values_options.present?
162 keyword = keyword.to_s.downcase
162 keyword = keyword.to_s.downcase
163 possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
163 possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
164 else
164 else
165 keyword
165 keyword
166 end
166 end
167 end
167 end
168
168
169 # Returns a ORDER BY clause that can used to sort customized
169 # Returns a ORDER BY clause that can used to sort customized
170 # objects by their value of the custom field.
170 # objects by their value of the custom field.
171 # Returns nil if the custom field can not be used for sorting.
171 # Returns nil if the custom field can not be used for sorting.
172 def order_statement
172 def order_statement
173 return nil if multiple?
173 return nil if multiple?
174 case field_format
174 case field_format
175 when 'string', 'text', 'list', 'date', 'bool'
175 when 'string', 'text', 'list', 'date', 'bool'
176 # COALESCE is here to make sure that blank and NULL values are sorted equally
176 # COALESCE is here to make sure that blank and NULL values are sorted equally
177 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
177 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
178 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
178 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
179 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
179 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
180 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
180 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
181 when 'int', 'float'
181 when 'int', 'float'
182 # Make the database cast values into numeric
182 # Make the database cast values into numeric
183 # Postgresql will raise an error if a value can not be casted!
183 # Postgresql will raise an error if a value can not be casted!
184 # CustomValue validations should ensure that it doesn't occur
184 # CustomValue validations should ensure that it doesn't occur
185 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
185 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
186 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
186 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
187 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
187 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
188 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
188 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
189 when 'user', 'version'
189 when 'user', 'version'
190 value_class.fields_for_order_statement(value_join_alias)
190 value_class.fields_for_order_statement(value_join_alias)
191 else
191 else
192 nil
192 nil
193 end
193 end
194 end
194 end
195
195
196 # Returns a GROUP BY clause that can used to group by custom value
196 # Returns a GROUP BY clause that can used to group by custom value
197 # Returns nil if the custom field can not be used for grouping.
197 # Returns nil if the custom field can not be used for grouping.
198 def group_statement
198 def group_statement
199 return nil if multiple?
199 return nil if multiple?
200 case field_format
200 case field_format
201 when 'list', 'date', 'bool', 'int'
201 when 'list', 'date', 'bool', 'int'
202 order_statement
202 order_statement
203 when 'user', 'version'
203 when 'user', 'version'
204 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
204 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
205 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
205 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
206 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
206 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
207 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
207 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
208 else
208 else
209 nil
209 nil
210 end
210 end
211 end
211 end
212
212
213 def join_for_order_statement
213 def join_for_order_statement
214 case field_format
214 case field_format
215 when 'user', 'version'
215 when 'user', 'version'
216 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
216 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
217 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
217 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
218 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
218 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
219 " AND #{join_alias}.custom_field_id = #{id}" +
219 " AND #{join_alias}.custom_field_id = #{id}" +
220 " AND #{join_alias}.value <> ''" +
220 " AND #{join_alias}.value <> ''" +
221 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
221 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
222 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
222 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
223 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
223 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
224 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
224 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
225 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
225 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
226 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
226 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
227 else
227 else
228 nil
228 nil
229 end
229 end
230 end
230 end
231
231
232 def join_alias
232 def join_alias
233 "cf_#{id}"
233 "cf_#{id}"
234 end
234 end
235
235
236 def value_join_alias
236 def value_join_alias
237 join_alias + "_" + field_format
237 join_alias + "_" + field_format
238 end
238 end
239
239
240 def <=>(field)
240 def <=>(field)
241 position <=> field.position
241 position <=> field.position
242 end
242 end
243
243
244 # Returns the class that values represent
244 # Returns the class that values represent
245 def value_class
245 def value_class
246 case field_format
246 case field_format
247 when 'user', 'version'
247 when 'user', 'version'
248 field_format.classify.constantize
248 field_format.classify.constantize
249 else
249 else
250 nil
250 nil
251 end
251 end
252 end
252 end
253
253
254 def self.customized_class
254 def self.customized_class
255 self.name =~ /^(.+)CustomField$/
255 self.name =~ /^(.+)CustomField$/
256 begin; $1.constantize; rescue nil; end
256 begin; $1.constantize; rescue nil; end
257 end
257 end
258
258
259 # to move in project_custom_field
259 # to move in project_custom_field
260 def self.for_all
260 def self.for_all
261 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
261 where(:is_for_all => true).order('position').all
262 end
262 end
263
263
264 def type_name
264 def type_name
265 nil
265 nil
266 end
266 end
267
267
268 # Returns the error messages for the given value
268 # Returns the error messages for the given value
269 # or an empty array if value is a valid value for the custom field
269 # or an empty array if value is a valid value for the custom field
270 def validate_field_value(value)
270 def validate_field_value(value)
271 errs = []
271 errs = []
272 if value.is_a?(Array)
272 if value.is_a?(Array)
273 if !multiple?
273 if !multiple?
274 errs << ::I18n.t('activerecord.errors.messages.invalid')
274 errs << ::I18n.t('activerecord.errors.messages.invalid')
275 end
275 end
276 if is_required? && value.detect(&:present?).nil?
276 if is_required? && value.detect(&:present?).nil?
277 errs << ::I18n.t('activerecord.errors.messages.blank')
277 errs << ::I18n.t('activerecord.errors.messages.blank')
278 end
278 end
279 value.each {|v| errs += validate_field_value_format(v)}
279 value.each {|v| errs += validate_field_value_format(v)}
280 else
280 else
281 if is_required? && value.blank?
281 if is_required? && value.blank?
282 errs << ::I18n.t('activerecord.errors.messages.blank')
282 errs << ::I18n.t('activerecord.errors.messages.blank')
283 end
283 end
284 errs += validate_field_value_format(value)
284 errs += validate_field_value_format(value)
285 end
285 end
286 errs
286 errs
287 end
287 end
288
288
289 # Returns true if value is a valid value for the custom field
289 # Returns true if value is a valid value for the custom field
290 def valid_field_value?(value)
290 def valid_field_value?(value)
291 validate_field_value(value).empty?
291 validate_field_value(value).empty?
292 end
292 end
293
293
294 def format_in?(*args)
294 def format_in?(*args)
295 args.include?(field_format)
295 args.include?(field_format)
296 end
296 end
297
297
298 protected
298 protected
299
299
300 # Returns the error message for the given value regarding its format
300 # Returns the error message for the given value regarding its format
301 def validate_field_value_format(value)
301 def validate_field_value_format(value)
302 errs = []
302 errs = []
303 if value.present?
303 if value.present?
304 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
304 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
305 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
305 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
306 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
306 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
307
307
308 # Format specific validations
308 # Format specific validations
309 case field_format
309 case field_format
310 when 'int'
310 when 'int'
311 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
311 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
312 when 'float'
312 when 'float'
313 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
313 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
314 when 'date'
314 when 'date'
315 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
315 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
316 when 'list'
316 when 'list'
317 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
317 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
318 end
318 end
319 end
319 end
320 errs
320 errs
321 end
321 end
322 end
322 end
@@ -1,86 +1,89
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Group < Principal
18 class Group < Principal
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 has_and_belongs_to_many :users, :after_add => :user_added,
21 has_and_belongs_to_many :users, :after_add => :user_added,
22 :after_remove => :user_removed
22 :after_remove => :user_removed
23
23
24 acts_as_customizable
24 acts_as_customizable
25
25
26 validates_presence_of :lastname
26 validates_presence_of :lastname
27 validates_uniqueness_of :lastname, :case_sensitive => false
27 validates_uniqueness_of :lastname, :case_sensitive => false
28 validates_length_of :lastname, :maximum => 30
28 validates_length_of :lastname, :maximum => 30
29
29
30 before_destroy :remove_references_before_destroy
30 before_destroy :remove_references_before_destroy
31
31
32 scope :sorted, order("#{table_name}.lastname ASC")
32 scope :sorted, order("#{table_name}.lastname ASC")
33
33
34 safe_attributes 'name',
34 safe_attributes 'name',
35 'user_ids',
35 'user_ids',
36 'custom_field_values',
36 'custom_field_values',
37 'custom_fields',
37 'custom_fields',
38 :if => lambda {|group, user| user.admin?}
38 :if => lambda {|group, user| user.admin?}
39
39
40 def to_s
40 def to_s
41 lastname.to_s
41 lastname.to_s
42 end
42 end
43
43
44 def name
44 def name
45 lastname
45 lastname
46 end
46 end
47
47
48 def name=(arg)
48 def name=(arg)
49 self.lastname = arg
49 self.lastname = arg
50 end
50 end
51
51
52 def user_added(user)
52 def user_added(user)
53 members.each do |member|
53 members.each do |member|
54 next if member.project.nil?
54 next if member.project.nil?
55 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
55 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
56 member.member_roles.each do |member_role|
56 member.member_roles.each do |member_role|
57 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
57 user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
58 end
58 end
59 user_member.save!
59 user_member.save!
60 end
60 end
61 end
61 end
62
62
63 def user_removed(user)
63 def user_removed(user)
64 members.each do |member|
64 members.each do |member|
65 MemberRole.find(:all, :include => :member,
65 MemberRole.
66 :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
66 includes(:member).
67 where("#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids).
68 all.
69 each(&:destroy)
67 end
70 end
68 end
71 end
69
72
70 def self.human_attribute_name(attribute_key_name, *args)
73 def self.human_attribute_name(attribute_key_name, *args)
71 attr_name = attribute_key_name.to_s
74 attr_name = attribute_key_name.to_s
72 if attr_name == 'lastname'
75 if attr_name == 'lastname'
73 attr_name = "name"
76 attr_name = "name"
74 end
77 end
75 super(attr_name, *args)
78 super(attr_name, *args)
76 end
79 end
77
80
78 private
81 private
79
82
80 # Removes references that are not handled by associations
83 # Removes references that are not handled by associations
81 def remove_references_before_destroy
84 def remove_references_before_destroy
82 return if self.id.nil?
85 return if self.id.nil?
83
86
84 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
87 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
85 end
88 end
86 end
89 end
@@ -1,498 +1,498
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 if @@handler_options[:allow_override].is_a?(String)
32 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 @@handler_options[:allow_override] ||= []
35 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
40
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42
42
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 super(email)
44 super(email)
45 end
45 end
46
46
47 def logger
47 def logger
48 Rails.logger
48 Rails.logger
49 end
49 end
50
50
51 cattr_accessor :ignored_emails_headers
51 cattr_accessor :ignored_emails_headers
52 @@ignored_emails_headers = {
52 @@ignored_emails_headers = {
53 'X-Auto-Response-Suppress' => 'oof',
53 'X-Auto-Response-Suppress' => 'oof',
54 'Auto-Submitted' => /^auto-/
54 'Auto-Submitted' => /^auto-/
55 }
55 }
56
56
57 # Processes incoming emails
57 # Processes incoming emails
58 # Returns the created object (eg. an issue, a message) or false
58 # Returns the created object (eg. an issue, a message) or false
59 def receive(email)
59 def receive(email)
60 @email = email
60 @email = email
61 sender_email = email.from.to_a.first.to_s.strip
61 sender_email = email.from.to_a.first.to_s.strip
62 # Ignore emails received from the application emission address to avoid hell cycles
62 # Ignore emails received from the application emission address to avoid hell cycles
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 if logger && logger.info
64 if logger && logger.info
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 end
66 end
67 return false
67 return false
68 end
68 end
69 # Ignore auto generated emails
69 # Ignore auto generated emails
70 self.class.ignored_emails_headers.each do |key, ignored_value|
70 self.class.ignored_emails_headers.each do |key, ignored_value|
71 value = email.header[key]
71 value = email.header[key]
72 if value
72 if value
73 value = value.to_s.downcase
73 value = value.to_s.downcase
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
75 if logger && logger.info
75 if logger && logger.info
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 end
77 end
78 return false
78 return false
79 end
79 end
80 end
80 end
81 end
81 end
82 @user = User.find_by_mail(sender_email) if sender_email.present?
82 @user = User.find_by_mail(sender_email) if sender_email.present?
83 if @user && !@user.active?
83 if @user && !@user.active?
84 if logger && logger.info
84 if logger && logger.info
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
86 end
86 end
87 return false
87 return false
88 end
88 end
89 if @user.nil?
89 if @user.nil?
90 # Email was submitted by an unknown user
90 # Email was submitted by an unknown user
91 case @@handler_options[:unknown_user]
91 case @@handler_options[:unknown_user]
92 when 'accept'
92 when 'accept'
93 @user = User.anonymous
93 @user = User.anonymous
94 when 'create'
94 when 'create'
95 @user = create_user_from_email
95 @user = create_user_from_email
96 if @user
96 if @user
97 if logger && logger.info
97 if logger && logger.info
98 logger.info "MailHandler: [#{@user.login}] account created"
98 logger.info "MailHandler: [#{@user.login}] account created"
99 end
99 end
100 Mailer.account_information(@user, @user.password).deliver
100 Mailer.account_information(@user, @user.password).deliver
101 else
101 else
102 if logger && logger.error
102 if logger && logger.error
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
104 end
104 end
105 return false
105 return false
106 end
106 end
107 else
107 else
108 # Default behaviour, emails from unknown users are ignored
108 # Default behaviour, emails from unknown users are ignored
109 if logger && logger.info
109 if logger && logger.info
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
111 end
111 end
112 return false
112 return false
113 end
113 end
114 end
114 end
115 User.current = @user
115 User.current = @user
116 dispatch
116 dispatch
117 end
117 end
118
118
119 private
119 private
120
120
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
124
124
125 def dispatch
125 def dispatch
126 headers = [email.in_reply_to, email.references].flatten.compact
126 headers = [email.in_reply_to, email.references].flatten.compact
127 subject = email.subject.to_s
127 subject = email.subject.to_s
128 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
128 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
129 klass, object_id = $1, $2.to_i
129 klass, object_id = $1, $2.to_i
130 method_name = "receive_#{klass}_reply"
130 method_name = "receive_#{klass}_reply"
131 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
131 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
132 send method_name, object_id
132 send method_name, object_id
133 else
133 else
134 # ignoring it
134 # ignoring it
135 end
135 end
136 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
136 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
137 receive_issue_reply(m[1].to_i)
137 receive_issue_reply(m[1].to_i)
138 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
138 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
139 receive_message_reply(m[1].to_i)
139 receive_message_reply(m[1].to_i)
140 else
140 else
141 dispatch_to_default
141 dispatch_to_default
142 end
142 end
143 rescue ActiveRecord::RecordInvalid => e
143 rescue ActiveRecord::RecordInvalid => e
144 # TODO: send a email to the user
144 # TODO: send a email to the user
145 logger.error e.message if logger
145 logger.error e.message if logger
146 false
146 false
147 rescue MissingInformation => e
147 rescue MissingInformation => e
148 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
148 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
149 false
149 false
150 rescue UnauthorizedAction => e
150 rescue UnauthorizedAction => e
151 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
151 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
152 false
152 false
153 end
153 end
154
154
155 def dispatch_to_default
155 def dispatch_to_default
156 receive_issue
156 receive_issue
157 end
157 end
158
158
159 # Creates a new issue
159 # Creates a new issue
160 def receive_issue
160 def receive_issue
161 project = target_project
161 project = target_project
162 # check permission
162 # check permission
163 unless @@handler_options[:no_permission_check]
163 unless @@handler_options[:no_permission_check]
164 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
164 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
165 end
165 end
166
166
167 issue = Issue.new(:author => user, :project => project)
167 issue = Issue.new(:author => user, :project => project)
168 issue.safe_attributes = issue_attributes_from_keywords(issue)
168 issue.safe_attributes = issue_attributes_from_keywords(issue)
169 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
169 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
170 issue.subject = cleaned_up_subject
170 issue.subject = cleaned_up_subject
171 if issue.subject.blank?
171 if issue.subject.blank?
172 issue.subject = '(no subject)'
172 issue.subject = '(no subject)'
173 end
173 end
174 issue.description = cleaned_up_text_body
174 issue.description = cleaned_up_text_body
175
175
176 # add To and Cc as watchers before saving so the watchers can reply to Redmine
176 # add To and Cc as watchers before saving so the watchers can reply to Redmine
177 add_watchers(issue)
177 add_watchers(issue)
178 issue.save!
178 issue.save!
179 add_attachments(issue)
179 add_attachments(issue)
180 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
180 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
181 issue
181 issue
182 end
182 end
183
183
184 # Adds a note to an existing issue
184 # Adds a note to an existing issue
185 def receive_issue_reply(issue_id, from_journal=nil)
185 def receive_issue_reply(issue_id, from_journal=nil)
186 issue = Issue.find_by_id(issue_id)
186 issue = Issue.find_by_id(issue_id)
187 return unless issue
187 return unless issue
188 # check permission
188 # check permission
189 unless @@handler_options[:no_permission_check]
189 unless @@handler_options[:no_permission_check]
190 unless user.allowed_to?(:add_issue_notes, issue.project) ||
190 unless user.allowed_to?(:add_issue_notes, issue.project) ||
191 user.allowed_to?(:edit_issues, issue.project)
191 user.allowed_to?(:edit_issues, issue.project)
192 raise UnauthorizedAction
192 raise UnauthorizedAction
193 end
193 end
194 end
194 end
195
195
196 # ignore CLI-supplied defaults for new issues
196 # ignore CLI-supplied defaults for new issues
197 @@handler_options[:issue].clear
197 @@handler_options[:issue].clear
198
198
199 journal = issue.init_journal(user)
199 journal = issue.init_journal(user)
200 if from_journal && from_journal.private_notes?
200 if from_journal && from_journal.private_notes?
201 # If the received email was a reply to a private note, make the added note private
201 # If the received email was a reply to a private note, make the added note private
202 issue.private_notes = true
202 issue.private_notes = true
203 end
203 end
204 issue.safe_attributes = issue_attributes_from_keywords(issue)
204 issue.safe_attributes = issue_attributes_from_keywords(issue)
205 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
205 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
206 journal.notes = cleaned_up_text_body
206 journal.notes = cleaned_up_text_body
207 add_attachments(issue)
207 add_attachments(issue)
208 issue.save!
208 issue.save!
209 if logger && logger.info
209 if logger && logger.info
210 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
210 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
211 end
211 end
212 journal
212 journal
213 end
213 end
214
214
215 # Reply will be added to the issue
215 # Reply will be added to the issue
216 def receive_journal_reply(journal_id)
216 def receive_journal_reply(journal_id)
217 journal = Journal.find_by_id(journal_id)
217 journal = Journal.find_by_id(journal_id)
218 if journal && journal.journalized_type == 'Issue'
218 if journal && journal.journalized_type == 'Issue'
219 receive_issue_reply(journal.journalized_id, journal)
219 receive_issue_reply(journal.journalized_id, journal)
220 end
220 end
221 end
221 end
222
222
223 # Receives a reply to a forum message
223 # Receives a reply to a forum message
224 def receive_message_reply(message_id)
224 def receive_message_reply(message_id)
225 message = Message.find_by_id(message_id)
225 message = Message.find_by_id(message_id)
226 if message
226 if message
227 message = message.root
227 message = message.root
228
228
229 unless @@handler_options[:no_permission_check]
229 unless @@handler_options[:no_permission_check]
230 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
230 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
231 end
231 end
232
232
233 if !message.locked?
233 if !message.locked?
234 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
234 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
235 :content => cleaned_up_text_body)
235 :content => cleaned_up_text_body)
236 reply.author = user
236 reply.author = user
237 reply.board = message.board
237 reply.board = message.board
238 message.children << reply
238 message.children << reply
239 add_attachments(reply)
239 add_attachments(reply)
240 reply
240 reply
241 else
241 else
242 if logger && logger.info
242 if logger && logger.info
243 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
243 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
244 end
244 end
245 end
245 end
246 end
246 end
247 end
247 end
248
248
249 def add_attachments(obj)
249 def add_attachments(obj)
250 if email.attachments && email.attachments.any?
250 if email.attachments && email.attachments.any?
251 email.attachments.each do |attachment|
251 email.attachments.each do |attachment|
252 filename = attachment.filename
252 filename = attachment.filename
253 unless filename.respond_to?(:encoding)
253 unless filename.respond_to?(:encoding)
254 # try to reencode to utf8 manually with ruby1.8
254 # try to reencode to utf8 manually with ruby1.8
255 h = attachment.header['Content-Disposition']
255 h = attachment.header['Content-Disposition']
256 unless h.nil?
256 unless h.nil?
257 begin
257 begin
258 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
258 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
259 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
259 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
260 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
260 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
261 # http://tools.ietf.org/html/rfc2047#section-4
261 # http://tools.ietf.org/html/rfc2047#section-4
262 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
262 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
263 end
263 end
264 rescue
264 rescue
265 # nop
265 # nop
266 end
266 end
267 end
267 end
268 end
268 end
269 obj.attachments << Attachment.create(:container => obj,
269 obj.attachments << Attachment.create(:container => obj,
270 :file => attachment.decoded,
270 :file => attachment.decoded,
271 :filename => filename,
271 :filename => filename,
272 :author => user,
272 :author => user,
273 :content_type => attachment.mime_type)
273 :content_type => attachment.mime_type)
274 end
274 end
275 end
275 end
276 end
276 end
277
277
278 # Adds To and Cc as watchers of the given object if the sender has the
278 # Adds To and Cc as watchers of the given object if the sender has the
279 # appropriate permission
279 # appropriate permission
280 def add_watchers(obj)
280 def add_watchers(obj)
281 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
281 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
282 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
282 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
283 unless addresses.empty?
283 unless addresses.empty?
284 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
284 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
285 watchers.each {|w| obj.add_watcher(w)}
285 watchers.each {|w| obj.add_watcher(w)}
286 end
286 end
287 end
287 end
288 end
288 end
289
289
290 def get_keyword(attr, options={})
290 def get_keyword(attr, options={})
291 @keywords ||= {}
291 @keywords ||= {}
292 if @keywords.has_key?(attr)
292 if @keywords.has_key?(attr)
293 @keywords[attr]
293 @keywords[attr]
294 else
294 else
295 @keywords[attr] = begin
295 @keywords[attr] = begin
296 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
296 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
297 (v = extract_keyword!(plain_text_body, attr, options[:format]))
297 (v = extract_keyword!(plain_text_body, attr, options[:format]))
298 v
298 v
299 elsif !@@handler_options[:issue][attr].blank?
299 elsif !@@handler_options[:issue][attr].blank?
300 @@handler_options[:issue][attr]
300 @@handler_options[:issue][attr]
301 end
301 end
302 end
302 end
303 end
303 end
304 end
304 end
305
305
306 # Destructively extracts the value for +attr+ in +text+
306 # Destructively extracts the value for +attr+ in +text+
307 # Returns nil if no matching keyword found
307 # Returns nil if no matching keyword found
308 def extract_keyword!(text, attr, format=nil)
308 def extract_keyword!(text, attr, format=nil)
309 keys = [attr.to_s.humanize]
309 keys = [attr.to_s.humanize]
310 if attr.is_a?(Symbol)
310 if attr.is_a?(Symbol)
311 if user && user.language.present?
311 if user && user.language.present?
312 keys << l("field_#{attr}", :default => '', :locale => user.language)
312 keys << l("field_#{attr}", :default => '', :locale => user.language)
313 end
313 end
314 if Setting.default_language.present?
314 if Setting.default_language.present?
315 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
315 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
316 end
316 end
317 end
317 end
318 keys.reject! {|k| k.blank?}
318 keys.reject! {|k| k.blank?}
319 keys.collect! {|k| Regexp.escape(k)}
319 keys.collect! {|k| Regexp.escape(k)}
320 format ||= '.+'
320 format ||= '.+'
321 keyword = nil
321 keyword = nil
322 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
322 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
323 if m = text.match(regexp)
323 if m = text.match(regexp)
324 keyword = m[2].strip
324 keyword = m[2].strip
325 text.gsub!(regexp, '')
325 text.gsub!(regexp, '')
326 end
326 end
327 keyword
327 keyword
328 end
328 end
329
329
330 def target_project
330 def target_project
331 # TODO: other ways to specify project:
331 # TODO: other ways to specify project:
332 # * parse the email To field
332 # * parse the email To field
333 # * specific project (eg. Setting.mail_handler_target_project)
333 # * specific project (eg. Setting.mail_handler_target_project)
334 target = Project.find_by_identifier(get_keyword(:project))
334 target = Project.find_by_identifier(get_keyword(:project))
335 raise MissingInformation.new('Unable to determine target project') if target.nil?
335 raise MissingInformation.new('Unable to determine target project') if target.nil?
336 target
336 target
337 end
337 end
338
338
339 # Returns a Hash of issue attributes extracted from keywords in the email body
339 # Returns a Hash of issue attributes extracted from keywords in the email body
340 def issue_attributes_from_keywords(issue)
340 def issue_attributes_from_keywords(issue)
341 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
341 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
342
342
343 attrs = {
343 attrs = {
344 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
344 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
345 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
345 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
346 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
346 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
347 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
347 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
348 'assigned_to_id' => assigned_to.try(:id),
348 'assigned_to_id' => assigned_to.try(:id),
349 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
349 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
350 issue.project.shared_versions.named(k).first.try(:id),
350 issue.project.shared_versions.named(k).first.try(:id),
351 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
351 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
352 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
352 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
353 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
353 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
354 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
354 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
355 }.delete_if {|k, v| v.blank? }
355 }.delete_if {|k, v| v.blank? }
356
356
357 if issue.new_record? && attrs['tracker_id'].nil?
357 if issue.new_record? && attrs['tracker_id'].nil?
358 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
358 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
359 end
359 end
360
360
361 attrs
361 attrs
362 end
362 end
363
363
364 # Returns a Hash of issue custom field values extracted from keywords in the email body
364 # Returns a Hash of issue custom field values extracted from keywords in the email body
365 def custom_field_values_from_keywords(customized)
365 def custom_field_values_from_keywords(customized)
366 customized.custom_field_values.inject({}) do |h, v|
366 customized.custom_field_values.inject({}) do |h, v|
367 if keyword = get_keyword(v.custom_field.name, :override => true)
367 if keyword = get_keyword(v.custom_field.name, :override => true)
368 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
368 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
369 end
369 end
370 h
370 h
371 end
371 end
372 end
372 end
373
373
374 # Returns the text/plain part of the email
374 # Returns the text/plain part of the email
375 # If not found (eg. HTML-only email), returns the body with tags removed
375 # If not found (eg. HTML-only email), returns the body with tags removed
376 def plain_text_body
376 def plain_text_body
377 return @plain_text_body unless @plain_text_body.nil?
377 return @plain_text_body unless @plain_text_body.nil?
378
378
379 part = email.text_part || email.html_part || email
379 part = email.text_part || email.html_part || email
380 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
380 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
381
381
382 # strip html tags and remove doctype directive
382 # strip html tags and remove doctype directive
383 @plain_text_body = strip_tags(@plain_text_body.strip)
383 @plain_text_body = strip_tags(@plain_text_body.strip)
384 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
384 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
385 @plain_text_body
385 @plain_text_body
386 end
386 end
387
387
388 def cleaned_up_text_body
388 def cleaned_up_text_body
389 cleanup_body(plain_text_body)
389 cleanup_body(plain_text_body)
390 end
390 end
391
391
392 def cleaned_up_subject
392 def cleaned_up_subject
393 subject = email.subject.to_s
393 subject = email.subject.to_s
394 unless subject.respond_to?(:encoding)
394 unless subject.respond_to?(:encoding)
395 # try to reencode to utf8 manually with ruby1.8
395 # try to reencode to utf8 manually with ruby1.8
396 begin
396 begin
397 if h = email.header[:subject]
397 if h = email.header[:subject]
398 # http://tools.ietf.org/html/rfc2047#section-4
398 # http://tools.ietf.org/html/rfc2047#section-4
399 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
399 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
400 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
400 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
401 end
401 end
402 end
402 end
403 rescue
403 rescue
404 # nop
404 # nop
405 end
405 end
406 end
406 end
407 subject.strip[0,255]
407 subject.strip[0,255]
408 end
408 end
409
409
410 def self.full_sanitizer
410 def self.full_sanitizer
411 @full_sanitizer ||= HTML::FullSanitizer.new
411 @full_sanitizer ||= HTML::FullSanitizer.new
412 end
412 end
413
413
414 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
414 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
415 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
415 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
416 value = value.to_s.slice(0, limit)
416 value = value.to_s.slice(0, limit)
417 object.send("#{attribute}=", value)
417 object.send("#{attribute}=", value)
418 end
418 end
419
419
420 # Returns a User from an email address and a full name
420 # Returns a User from an email address and a full name
421 def self.new_user_from_attributes(email_address, fullname=nil)
421 def self.new_user_from_attributes(email_address, fullname=nil)
422 user = User.new
422 user = User.new
423
423
424 # Truncating the email address would result in an invalid format
424 # Truncating the email address would result in an invalid format
425 user.mail = email_address
425 user.mail = email_address
426 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
426 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
427
427
428 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
428 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
429 assign_string_attribute_with_limit(user, 'firstname', names.shift)
429 assign_string_attribute_with_limit(user, 'firstname', names.shift)
430 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
430 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
431 user.lastname = '-' if user.lastname.blank?
431 user.lastname = '-' if user.lastname.blank?
432
432
433 password_length = [Setting.password_min_length.to_i, 10].max
433 password_length = [Setting.password_min_length.to_i, 10].max
434 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
434 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
435 user.language = Setting.default_language
435 user.language = Setting.default_language
436
436
437 unless user.valid?
437 unless user.valid?
438 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
438 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
439 user.firstname = "-" unless user.errors[:firstname].blank?
439 user.firstname = "-" unless user.errors[:firstname].blank?
440 user.lastname = "-" unless user.errors[:lastname].blank?
440 user.lastname = "-" unless user.errors[:lastname].blank?
441 end
441 end
442
442
443 user
443 user
444 end
444 end
445
445
446 # Creates a User for the +email+ sender
446 # Creates a User for the +email+ sender
447 # Returns the user or nil if it could not be created
447 # Returns the user or nil if it could not be created
448 def create_user_from_email
448 def create_user_from_email
449 from = email.header['from'].to_s
449 from = email.header['from'].to_s
450 addr, name = from, nil
450 addr, name = from, nil
451 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
451 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
452 addr, name = m[2], m[1]
452 addr, name = m[2], m[1]
453 end
453 end
454 if addr.present?
454 if addr.present?
455 user = self.class.new_user_from_attributes(addr, name)
455 user = self.class.new_user_from_attributes(addr, name)
456 if user.save
456 if user.save
457 user
457 user
458 else
458 else
459 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
459 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
460 nil
460 nil
461 end
461 end
462 else
462 else
463 logger.error "MailHandler: failed to create User: no FROM address found" if logger
463 logger.error "MailHandler: failed to create User: no FROM address found" if logger
464 nil
464 nil
465 end
465 end
466 end
466 end
467
467
468 # Removes the email body of text after the truncation configurations.
468 # Removes the email body of text after the truncation configurations.
469 def cleanup_body(body)
469 def cleanup_body(body)
470 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
470 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
471 unless delimiters.empty?
471 unless delimiters.empty?
472 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
472 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
473 body = body.gsub(regex, '')
473 body = body.gsub(regex, '')
474 end
474 end
475 body.strip
475 body.strip
476 end
476 end
477
477
478 def find_assignee_from_keyword(keyword, issue)
478 def find_assignee_from_keyword(keyword, issue)
479 keyword = keyword.to_s.downcase
479 keyword = keyword.to_s.downcase
480 assignable = issue.assignable_users
480 assignable = issue.assignable_users
481 assignee = nil
481 assignee = nil
482 assignee ||= assignable.detect {|a|
482 assignee ||= assignable.detect {|a|
483 a.mail.to_s.downcase == keyword ||
483 a.mail.to_s.downcase == keyword ||
484 a.login.to_s.downcase == keyword
484 a.login.to_s.downcase == keyword
485 }
485 }
486 if assignee.nil? && keyword.match(/ /)
486 if assignee.nil? && keyword.match(/ /)
487 firstname, lastname = *(keyword.split) # "First Last Throwaway"
487 firstname, lastname = *(keyword.split) # "First Last Throwaway"
488 assignee ||= assignable.detect {|a|
488 assignee ||= assignable.detect {|a|
489 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
489 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
490 a.lastname.to_s.downcase == lastname
490 a.lastname.to_s.downcase == lastname
491 }
491 }
492 end
492 end
493 if assignee.nil?
493 if assignee.nil?
494 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
494 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
495 end
495 end
496 assignee
496 assignee
497 end
497 end
498 end
498 end
@@ -1,474 +1,474
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Mailer < ActionMailer::Base
18 class Mailer < ActionMailer::Base
19 layout 'mailer'
19 layout 'mailer'
20 helper :application
20 helper :application
21 helper :issues
21 helper :issues
22 helper :custom_fields
22 helper :custom_fields
23
23
24 include Redmine::I18n
24 include Redmine::I18n
25
25
26 def self.default_url_options
26 def self.default_url_options
27 { :host => Setting.host_name, :protocol => Setting.protocol }
27 { :host => Setting.host_name, :protocol => Setting.protocol }
28 end
28 end
29
29
30 # Builds a Mail::Message object used to email recipients of the added issue.
30 # Builds a Mail::Message object used to email recipients of the added issue.
31 #
31 #
32 # Example:
32 # Example:
33 # issue_add(issue) => Mail::Message object
33 # issue_add(issue) => Mail::Message object
34 # Mailer.issue_add(issue).deliver => sends an email to issue recipients
34 # Mailer.issue_add(issue).deliver => sends an email to issue recipients
35 def issue_add(issue)
35 def issue_add(issue)
36 redmine_headers 'Project' => issue.project.identifier,
36 redmine_headers 'Project' => issue.project.identifier,
37 'Issue-Id' => issue.id,
37 'Issue-Id' => issue.id,
38 'Issue-Author' => issue.author.login
38 'Issue-Author' => issue.author.login
39 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
39 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
40 message_id issue
40 message_id issue
41 @author = issue.author
41 @author = issue.author
42 @issue = issue
42 @issue = issue
43 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
43 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
44 recipients = issue.recipients
44 recipients = issue.recipients
45 cc = issue.watcher_recipients - recipients
45 cc = issue.watcher_recipients - recipients
46 mail :to => recipients,
46 mail :to => recipients,
47 :cc => cc,
47 :cc => cc,
48 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
48 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
49 end
49 end
50
50
51 # Builds a Mail::Message object used to email recipients of the edited issue.
51 # Builds a Mail::Message object used to email recipients of the edited issue.
52 #
52 #
53 # Example:
53 # Example:
54 # issue_edit(journal) => Mail::Message object
54 # issue_edit(journal) => Mail::Message object
55 # Mailer.issue_edit(journal).deliver => sends an email to issue recipients
55 # Mailer.issue_edit(journal).deliver => sends an email to issue recipients
56 def issue_edit(journal)
56 def issue_edit(journal)
57 issue = journal.journalized.reload
57 issue = journal.journalized.reload
58 redmine_headers 'Project' => issue.project.identifier,
58 redmine_headers 'Project' => issue.project.identifier,
59 'Issue-Id' => issue.id,
59 'Issue-Id' => issue.id,
60 'Issue-Author' => issue.author.login
60 'Issue-Author' => issue.author.login
61 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
61 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
62 message_id journal
62 message_id journal
63 references issue
63 references issue
64 @author = journal.user
64 @author = journal.user
65 recipients = journal.recipients
65 recipients = journal.recipients
66 # Watchers in cc
66 # Watchers in cc
67 cc = journal.watcher_recipients - recipients
67 cc = journal.watcher_recipients - recipients
68 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
68 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
69 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
69 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
70 s << issue.subject
70 s << issue.subject
71 @issue = issue
71 @issue = issue
72 @journal = journal
72 @journal = journal
73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
74 mail :to => recipients,
74 mail :to => recipients,
75 :cc => cc,
75 :cc => cc,
76 :subject => s
76 :subject => s
77 end
77 end
78
78
79 def reminder(user, issues, days)
79 def reminder(user, issues, days)
80 set_language_if_valid user.language
80 set_language_if_valid user.language
81 @issues = issues
81 @issues = issues
82 @days = days
82 @days = days
83 @issues_url = url_for(:controller => 'issues', :action => 'index',
83 @issues_url = url_for(:controller => 'issues', :action => 'index',
84 :set_filter => 1, :assigned_to_id => user.id,
84 :set_filter => 1, :assigned_to_id => user.id,
85 :sort => 'due_date:asc')
85 :sort => 'due_date:asc')
86 mail :to => user.mail,
86 mail :to => user.mail,
87 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
87 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
88 end
88 end
89
89
90 # Builds a Mail::Message object used to email users belonging to the added document's project.
90 # Builds a Mail::Message object used to email users belonging to the added document's project.
91 #
91 #
92 # Example:
92 # Example:
93 # document_added(document) => Mail::Message object
93 # document_added(document) => Mail::Message object
94 # Mailer.document_added(document).deliver => sends an email to the document's project recipients
94 # Mailer.document_added(document).deliver => sends an email to the document's project recipients
95 def document_added(document)
95 def document_added(document)
96 redmine_headers 'Project' => document.project.identifier
96 redmine_headers 'Project' => document.project.identifier
97 @author = User.current
97 @author = User.current
98 @document = document
98 @document = document
99 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
99 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
100 mail :to => document.recipients,
100 mail :to => document.recipients,
101 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
101 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
102 end
102 end
103
103
104 # Builds a Mail::Message object used to email recipients of a project when an attachements are added.
104 # Builds a Mail::Message object used to email recipients of a project when an attachements are added.
105 #
105 #
106 # Example:
106 # Example:
107 # attachments_added(attachments) => Mail::Message object
107 # attachments_added(attachments) => Mail::Message object
108 # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
108 # Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
109 def attachments_added(attachments)
109 def attachments_added(attachments)
110 container = attachments.first.container
110 container = attachments.first.container
111 added_to = ''
111 added_to = ''
112 added_to_url = ''
112 added_to_url = ''
113 @author = attachments.first.author
113 @author = attachments.first.author
114 case container.class.name
114 case container.class.name
115 when 'Project'
115 when 'Project'
116 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
116 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
117 added_to = "#{l(:label_project)}: #{container}"
117 added_to = "#{l(:label_project)}: #{container}"
118 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
118 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
119 when 'Version'
119 when 'Version'
120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
121 added_to = "#{l(:label_version)}: #{container.name}"
121 added_to = "#{l(:label_version)}: #{container.name}"
122 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
122 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
123 when 'Document'
123 when 'Document'
124 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
124 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
125 added_to = "#{l(:label_document)}: #{container.title}"
125 added_to = "#{l(:label_document)}: #{container.title}"
126 recipients = container.recipients
126 recipients = container.recipients
127 end
127 end
128 redmine_headers 'Project' => container.project.identifier
128 redmine_headers 'Project' => container.project.identifier
129 @attachments = attachments
129 @attachments = attachments
130 @added_to = added_to
130 @added_to = added_to
131 @added_to_url = added_to_url
131 @added_to_url = added_to_url
132 mail :to => recipients,
132 mail :to => recipients,
133 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
133 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
134 end
134 end
135
135
136 # Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
136 # Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
137 #
137 #
138 # Example:
138 # Example:
139 # news_added(news) => Mail::Message object
139 # news_added(news) => Mail::Message object
140 # Mailer.news_added(news).deliver => sends an email to the news' project recipients
140 # Mailer.news_added(news).deliver => sends an email to the news' project recipients
141 def news_added(news)
141 def news_added(news)
142 redmine_headers 'Project' => news.project.identifier
142 redmine_headers 'Project' => news.project.identifier
143 @author = news.author
143 @author = news.author
144 message_id news
144 message_id news
145 @news = news
145 @news = news
146 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
146 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
147 mail :to => news.recipients,
147 mail :to => news.recipients,
148 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
148 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
149 end
149 end
150
150
151 # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
151 # Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
152 #
152 #
153 # Example:
153 # Example:
154 # news_comment_added(comment) => Mail::Message object
154 # news_comment_added(comment) => Mail::Message object
155 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
155 # Mailer.news_comment_added(comment) => sends an email to the news' project recipients
156 def news_comment_added(comment)
156 def news_comment_added(comment)
157 news = comment.commented
157 news = comment.commented
158 redmine_headers 'Project' => news.project.identifier
158 redmine_headers 'Project' => news.project.identifier
159 @author = comment.author
159 @author = comment.author
160 message_id comment
160 message_id comment
161 @news = news
161 @news = news
162 @comment = comment
162 @comment = comment
163 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
163 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
164 mail :to => news.recipients,
164 mail :to => news.recipients,
165 :cc => news.watcher_recipients,
165 :cc => news.watcher_recipients,
166 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
166 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
167 end
167 end
168
168
169 # Builds a Mail::Message object used to email the recipients of the specified message that was posted.
169 # Builds a Mail::Message object used to email the recipients of the specified message that was posted.
170 #
170 #
171 # Example:
171 # Example:
172 # message_posted(message) => Mail::Message object
172 # message_posted(message) => Mail::Message object
173 # Mailer.message_posted(message).deliver => sends an email to the recipients
173 # Mailer.message_posted(message).deliver => sends an email to the recipients
174 def message_posted(message)
174 def message_posted(message)
175 redmine_headers 'Project' => message.project.identifier,
175 redmine_headers 'Project' => message.project.identifier,
176 'Topic-Id' => (message.parent_id || message.id)
176 'Topic-Id' => (message.parent_id || message.id)
177 @author = message.author
177 @author = message.author
178 message_id message
178 message_id message
179 references message.parent unless message.parent.nil?
179 references message.parent unless message.parent.nil?
180 recipients = message.recipients
180 recipients = message.recipients
181 cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
181 cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
182 @message = message
182 @message = message
183 @message_url = url_for(message.event_url)
183 @message_url = url_for(message.event_url)
184 mail :to => recipients,
184 mail :to => recipients,
185 :cc => cc,
185 :cc => cc,
186 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
186 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
187 end
187 end
188
188
189 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
189 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
190 #
190 #
191 # Example:
191 # Example:
192 # wiki_content_added(wiki_content) => Mail::Message object
192 # wiki_content_added(wiki_content) => Mail::Message object
193 # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
193 # Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
194 def wiki_content_added(wiki_content)
194 def wiki_content_added(wiki_content)
195 redmine_headers 'Project' => wiki_content.project.identifier,
195 redmine_headers 'Project' => wiki_content.project.identifier,
196 'Wiki-Page-Id' => wiki_content.page.id
196 'Wiki-Page-Id' => wiki_content.page.id
197 @author = wiki_content.author
197 @author = wiki_content.author
198 message_id wiki_content
198 message_id wiki_content
199 recipients = wiki_content.recipients
199 recipients = wiki_content.recipients
200 cc = wiki_content.page.wiki.watcher_recipients - recipients
200 cc = wiki_content.page.wiki.watcher_recipients - recipients
201 @wiki_content = wiki_content
201 @wiki_content = wiki_content
202 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
202 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
203 :project_id => wiki_content.project,
203 :project_id => wiki_content.project,
204 :id => wiki_content.page.title)
204 :id => wiki_content.page.title)
205 mail :to => recipients,
205 mail :to => recipients,
206 :cc => cc,
206 :cc => cc,
207 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
207 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
208 end
208 end
209
209
210 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
210 # Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
211 #
211 #
212 # Example:
212 # Example:
213 # wiki_content_updated(wiki_content) => Mail::Message object
213 # wiki_content_updated(wiki_content) => Mail::Message object
214 # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
214 # Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
215 def wiki_content_updated(wiki_content)
215 def wiki_content_updated(wiki_content)
216 redmine_headers 'Project' => wiki_content.project.identifier,
216 redmine_headers 'Project' => wiki_content.project.identifier,
217 'Wiki-Page-Id' => wiki_content.page.id
217 'Wiki-Page-Id' => wiki_content.page.id
218 @author = wiki_content.author
218 @author = wiki_content.author
219 message_id wiki_content
219 message_id wiki_content
220 recipients = wiki_content.recipients
220 recipients = wiki_content.recipients
221 cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
221 cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
222 @wiki_content = wiki_content
222 @wiki_content = wiki_content
223 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
223 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
224 :project_id => wiki_content.project,
224 :project_id => wiki_content.project,
225 :id => wiki_content.page.title)
225 :id => wiki_content.page.title)
226 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
226 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
227 :project_id => wiki_content.project, :id => wiki_content.page.title,
227 :project_id => wiki_content.project, :id => wiki_content.page.title,
228 :version => wiki_content.version)
228 :version => wiki_content.version)
229 mail :to => recipients,
229 mail :to => recipients,
230 :cc => cc,
230 :cc => cc,
231 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
231 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
232 end
232 end
233
233
234 # Builds a Mail::Message object used to email the specified user their account information.
234 # Builds a Mail::Message object used to email the specified user their account information.
235 #
235 #
236 # Example:
236 # Example:
237 # account_information(user, password) => Mail::Message object
237 # account_information(user, password) => Mail::Message object
238 # Mailer.account_information(user, password).deliver => sends account information to the user
238 # Mailer.account_information(user, password).deliver => sends account information to the user
239 def account_information(user, password)
239 def account_information(user, password)
240 set_language_if_valid user.language
240 set_language_if_valid user.language
241 @user = user
241 @user = user
242 @password = password
242 @password = password
243 @login_url = url_for(:controller => 'account', :action => 'login')
243 @login_url = url_for(:controller => 'account', :action => 'login')
244 mail :to => user.mail,
244 mail :to => user.mail,
245 :subject => l(:mail_subject_register, Setting.app_title)
245 :subject => l(:mail_subject_register, Setting.app_title)
246 end
246 end
247
247
248 # Builds a Mail::Message object used to email all active administrators of an account activation request.
248 # Builds a Mail::Message object used to email all active administrators of an account activation request.
249 #
249 #
250 # Example:
250 # Example:
251 # account_activation_request(user) => Mail::Message object
251 # account_activation_request(user) => Mail::Message object
252 # Mailer.account_activation_request(user).deliver => sends an email to all active administrators
252 # Mailer.account_activation_request(user).deliver => sends an email to all active administrators
253 def account_activation_request(user)
253 def account_activation_request(user)
254 # Send the email to all active administrators
254 # Send the email to all active administrators
255 recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
255 recipients = User.active.where(:admin => true).all.collect { |u| u.mail }.compact
256 @user = user
256 @user = user
257 @url = url_for(:controller => 'users', :action => 'index',
257 @url = url_for(:controller => 'users', :action => 'index',
258 :status => User::STATUS_REGISTERED,
258 :status => User::STATUS_REGISTERED,
259 :sort_key => 'created_on', :sort_order => 'desc')
259 :sort_key => 'created_on', :sort_order => 'desc')
260 mail :to => recipients,
260 mail :to => recipients,
261 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
261 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
262 end
262 end
263
263
264 # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
264 # Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
265 #
265 #
266 # Example:
266 # Example:
267 # account_activated(user) => Mail::Message object
267 # account_activated(user) => Mail::Message object
268 # Mailer.account_activated(user).deliver => sends an email to the registered user
268 # Mailer.account_activated(user).deliver => sends an email to the registered user
269 def account_activated(user)
269 def account_activated(user)
270 set_language_if_valid user.language
270 set_language_if_valid user.language
271 @user = user
271 @user = user
272 @login_url = url_for(:controller => 'account', :action => 'login')
272 @login_url = url_for(:controller => 'account', :action => 'login')
273 mail :to => user.mail,
273 mail :to => user.mail,
274 :subject => l(:mail_subject_register, Setting.app_title)
274 :subject => l(:mail_subject_register, Setting.app_title)
275 end
275 end
276
276
277 def lost_password(token)
277 def lost_password(token)
278 set_language_if_valid(token.user.language)
278 set_language_if_valid(token.user.language)
279 @token = token
279 @token = token
280 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
280 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
281 mail :to => token.user.mail,
281 mail :to => token.user.mail,
282 :subject => l(:mail_subject_lost_password, Setting.app_title)
282 :subject => l(:mail_subject_lost_password, Setting.app_title)
283 end
283 end
284
284
285 def register(token)
285 def register(token)
286 set_language_if_valid(token.user.language)
286 set_language_if_valid(token.user.language)
287 @token = token
287 @token = token
288 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
288 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
289 mail :to => token.user.mail,
289 mail :to => token.user.mail,
290 :subject => l(:mail_subject_register, Setting.app_title)
290 :subject => l(:mail_subject_register, Setting.app_title)
291 end
291 end
292
292
293 def test_email(user)
293 def test_email(user)
294 set_language_if_valid(user.language)
294 set_language_if_valid(user.language)
295 @url = url_for(:controller => 'welcome')
295 @url = url_for(:controller => 'welcome')
296 mail :to => user.mail,
296 mail :to => user.mail,
297 :subject => 'Redmine test'
297 :subject => 'Redmine test'
298 end
298 end
299
299
300 # Overrides default deliver! method to prevent from sending an email
300 # Overrides default deliver! method to prevent from sending an email
301 # with no recipient, cc or bcc
301 # with no recipient, cc or bcc
302 def deliver!(mail = @mail)
302 def deliver!(mail = @mail)
303 set_language_if_valid @initial_language
303 set_language_if_valid @initial_language
304 return false if (recipients.nil? || recipients.empty?) &&
304 return false if (recipients.nil? || recipients.empty?) &&
305 (cc.nil? || cc.empty?) &&
305 (cc.nil? || cc.empty?) &&
306 (bcc.nil? || bcc.empty?)
306 (bcc.nil? || bcc.empty?)
307
307
308
308
309 # Log errors when raise_delivery_errors is set to false, Rails does not
309 # Log errors when raise_delivery_errors is set to false, Rails does not
310 raise_errors = self.class.raise_delivery_errors
310 raise_errors = self.class.raise_delivery_errors
311 self.class.raise_delivery_errors = true
311 self.class.raise_delivery_errors = true
312 begin
312 begin
313 return super(mail)
313 return super(mail)
314 rescue Exception => e
314 rescue Exception => e
315 if raise_errors
315 if raise_errors
316 raise e
316 raise e
317 elsif mylogger
317 elsif mylogger
318 mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
318 mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
319 end
319 end
320 ensure
320 ensure
321 self.class.raise_delivery_errors = raise_errors
321 self.class.raise_delivery_errors = raise_errors
322 end
322 end
323 end
323 end
324
324
325 # Sends reminders to issue assignees
325 # Sends reminders to issue assignees
326 # Available options:
326 # Available options:
327 # * :days => how many days in the future to remind about (defaults to 7)
327 # * :days => how many days in the future to remind about (defaults to 7)
328 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
328 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
329 # * :project => id or identifier of project to process (defaults to all projects)
329 # * :project => id or identifier of project to process (defaults to all projects)
330 # * :users => array of user/group ids who should be reminded
330 # * :users => array of user/group ids who should be reminded
331 def self.reminders(options={})
331 def self.reminders(options={})
332 days = options[:days] || 7
332 days = options[:days] || 7
333 project = options[:project] ? Project.find(options[:project]) : nil
333 project = options[:project] ? Project.find(options[:project]) : nil
334 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
334 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
335 user_ids = options[:users]
335 user_ids = options[:users]
336
336
337 scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
337 scope = Issue.open.where("#{Issue.table_name}.assigned_to_id IS NOT NULL" +
338 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
338 " AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
339 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
339 " AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date
340 )
340 )
341 scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
341 scope = scope.where(:assigned_to_id => user_ids) if user_ids.present?
342 scope = scope.where(:project_id => project.id) if project
342 scope = scope.where(:project_id => project.id) if project
343 scope = scope.where(:tracker_id => tracker.id) if tracker
343 scope = scope.where(:tracker_id => tracker.id) if tracker
344
344
345 issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to)
345 issues_by_assignee = scope.includes(:status, :assigned_to, :project, :tracker).all.group_by(&:assigned_to)
346 issues_by_assignee.keys.each do |assignee|
346 issues_by_assignee.keys.each do |assignee|
347 if assignee.is_a?(Group)
347 if assignee.is_a?(Group)
348 assignee.users.each do |user|
348 assignee.users.each do |user|
349 issues_by_assignee[user] ||= []
349 issues_by_assignee[user] ||= []
350 issues_by_assignee[user] += issues_by_assignee[assignee]
350 issues_by_assignee[user] += issues_by_assignee[assignee]
351 end
351 end
352 end
352 end
353 end
353 end
354
354
355 issues_by_assignee.each do |assignee, issues|
355 issues_by_assignee.each do |assignee, issues|
356 reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
356 reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
357 end
357 end
358 end
358 end
359
359
360 # Activates/desactivates email deliveries during +block+
360 # Activates/desactivates email deliveries during +block+
361 def self.with_deliveries(enabled = true, &block)
361 def self.with_deliveries(enabled = true, &block)
362 was_enabled = ActionMailer::Base.perform_deliveries
362 was_enabled = ActionMailer::Base.perform_deliveries
363 ActionMailer::Base.perform_deliveries = !!enabled
363 ActionMailer::Base.perform_deliveries = !!enabled
364 yield
364 yield
365 ensure
365 ensure
366 ActionMailer::Base.perform_deliveries = was_enabled
366 ActionMailer::Base.perform_deliveries = was_enabled
367 end
367 end
368
368
369 # Sends emails synchronously in the given block
369 # Sends emails synchronously in the given block
370 def self.with_synched_deliveries(&block)
370 def self.with_synched_deliveries(&block)
371 saved_method = ActionMailer::Base.delivery_method
371 saved_method = ActionMailer::Base.delivery_method
372 if m = saved_method.to_s.match(%r{^async_(.+)$})
372 if m = saved_method.to_s.match(%r{^async_(.+)$})
373 synched_method = m[1]
373 synched_method = m[1]
374 ActionMailer::Base.delivery_method = synched_method.to_sym
374 ActionMailer::Base.delivery_method = synched_method.to_sym
375 ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
375 ActionMailer::Base.send "#{synched_method}_settings=", ActionMailer::Base.send("async_#{synched_method}_settings")
376 end
376 end
377 yield
377 yield
378 ensure
378 ensure
379 ActionMailer::Base.delivery_method = saved_method
379 ActionMailer::Base.delivery_method = saved_method
380 end
380 end
381
381
382 def mail(headers={})
382 def mail(headers={})
383 headers.merge! 'X-Mailer' => 'Redmine',
383 headers.merge! 'X-Mailer' => 'Redmine',
384 'X-Redmine-Host' => Setting.host_name,
384 'X-Redmine-Host' => Setting.host_name,
385 'X-Redmine-Site' => Setting.app_title,
385 'X-Redmine-Site' => Setting.app_title,
386 'X-Auto-Response-Suppress' => 'OOF',
386 'X-Auto-Response-Suppress' => 'OOF',
387 'Auto-Submitted' => 'auto-generated',
387 'Auto-Submitted' => 'auto-generated',
388 'From' => Setting.mail_from,
388 'From' => Setting.mail_from,
389 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
389 'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
390
390
391 # Removes the author from the recipients and cc
391 # Removes the author from the recipients and cc
392 # if he doesn't want to receive notifications about what he does
392 # if he doesn't want to receive notifications about what he does
393 if @author && @author.logged? && @author.pref[:no_self_notified]
393 if @author && @author.logged? && @author.pref[:no_self_notified]
394 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
394 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
395 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
395 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
396 end
396 end
397
397
398 if @author && @author.logged?
398 if @author && @author.logged?
399 redmine_headers 'Sender' => @author.login
399 redmine_headers 'Sender' => @author.login
400 end
400 end
401
401
402 # Blind carbon copy recipients
402 # Blind carbon copy recipients
403 if Setting.bcc_recipients?
403 if Setting.bcc_recipients?
404 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
404 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
405 headers[:to] = nil
405 headers[:to] = nil
406 headers[:cc] = nil
406 headers[:cc] = nil
407 end
407 end
408
408
409 if @message_id_object
409 if @message_id_object
410 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
410 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
411 end
411 end
412 if @references_objects
412 if @references_objects
413 headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
413 headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
414 end
414 end
415
415
416 super headers do |format|
416 super headers do |format|
417 format.text
417 format.text
418 format.html unless Setting.plain_text_mail?
418 format.html unless Setting.plain_text_mail?
419 end
419 end
420
420
421 set_language_if_valid @initial_language
421 set_language_if_valid @initial_language
422 end
422 end
423
423
424 def initialize(*args)
424 def initialize(*args)
425 @initial_language = current_language
425 @initial_language = current_language
426 set_language_if_valid Setting.default_language
426 set_language_if_valid Setting.default_language
427 super
427 super
428 end
428 end
429
429
430 def self.deliver_mail(mail)
430 def self.deliver_mail(mail)
431 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
431 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
432 super
432 super
433 end
433 end
434
434
435 def self.method_missing(method, *args, &block)
435 def self.method_missing(method, *args, &block)
436 if m = method.to_s.match(%r{^deliver_(.+)$})
436 if m = method.to_s.match(%r{^deliver_(.+)$})
437 ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
437 ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
438 send(m[1], *args).deliver
438 send(m[1], *args).deliver
439 else
439 else
440 super
440 super
441 end
441 end
442 end
442 end
443
443
444 private
444 private
445
445
446 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
446 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
447 def redmine_headers(h)
447 def redmine_headers(h)
448 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
448 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
449 end
449 end
450
450
451 # Returns a predictable Message-Id for the given object
451 # Returns a predictable Message-Id for the given object
452 def self.message_id_for(object)
452 def self.message_id_for(object)
453 # id + timestamp should reduce the odds of a collision
453 # id + timestamp should reduce the odds of a collision
454 # as far as we don't send multiple emails for the same object
454 # as far as we don't send multiple emails for the same object
455 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
455 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
456 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
456 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
457 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
457 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
458 host = "#{::Socket.gethostname}.redmine" if host.empty?
458 host = "#{::Socket.gethostname}.redmine" if host.empty?
459 "#{hash}@#{host}"
459 "#{hash}@#{host}"
460 end
460 end
461
461
462 def message_id(object)
462 def message_id(object)
463 @message_id_object = object
463 @message_id_object = object
464 end
464 end
465
465
466 def references(object)
466 def references(object)
467 @references_objects ||= []
467 @references_objects ||= []
468 @references_objects << object
468 @references_objects << object
469 end
469 end
470
470
471 def mylogger
471 def mylogger
472 Rails.logger
472 Rails.logger
473 end
473 end
474 end
474 end
@@ -1,64 +1,64
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 MemberRole < ActiveRecord::Base
18 class MemberRole < ActiveRecord::Base
19 belongs_to :member
19 belongs_to :member
20 belongs_to :role
20 belongs_to :role
21
21
22 after_destroy :remove_member_if_empty
22 after_destroy :remove_member_if_empty
23
23
24 after_create :add_role_to_group_users
24 after_create :add_role_to_group_users
25 after_destroy :remove_role_from_group_users
25 after_destroy :remove_role_from_group_users
26
26
27 validates_presence_of :role
27 validates_presence_of :role
28 validate :validate_role_member
28 validate :validate_role_member
29
29
30 def validate_role_member
30 def validate_role_member
31 errors.add :role_id, :invalid if role && !role.member?
31 errors.add :role_id, :invalid if role && !role.member?
32 end
32 end
33
33
34 def inherited?
34 def inherited?
35 !inherited_from.nil?
35 !inherited_from.nil?
36 end
36 end
37
37
38 private
38 private
39
39
40 def remove_member_if_empty
40 def remove_member_if_empty
41 if member.roles.empty?
41 if member.roles.empty?
42 member.destroy
42 member.destroy
43 end
43 end
44 end
44 end
45
45
46 def add_role_to_group_users
46 def add_role_to_group_users
47 if member.principal.is_a?(Group)
47 if member.principal.is_a?(Group)
48 member.principal.users.each do |user|
48 member.principal.users.each do |user|
49 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
49 user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
50 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
50 user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
51 user_member.save!
51 user_member.save!
52 end
52 end
53 end
53 end
54 end
54 end
55
55
56 def remove_role_from_group_users
56 def remove_role_from_group_users
57 MemberRole.find(:all, :conditions => { :inherited_from => id }).group_by(&:member).each do |member, member_roles|
57 MemberRole.where(:inherited_from => id).all.group_by(&:member).each do |member, member_roles|
58 member_roles.each(&:destroy)
58 member_roles.each(&:destroy)
59 if member && member.user
59 if member && member.user
60 Watcher.prune(:user => member.user, :project => member.project)
60 Watcher.prune(:user => member.user, :project => member.project)
61 end
61 end
62 end
62 end
63 end
63 end
64 end
64 end
@@ -1,974 +1,972
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
36 has_many :users, :through => :members
36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal
37 has_many :principals, :through => :member_principals, :source => :principal
38
38
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all
44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :dependent => :delete_all
45 has_many :queries, :dependent => :delete_all
46 has_many :documents, :dependent => :destroy
46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true]
50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
51 has_many :repositories, :dependent => :destroy
52 has_many :changesets, :through => :repository
52 has_many :changesets, :through => :repository
53 has_one :wiki, :dependent => :destroy
53 has_one :wiki, :dependent => :destroy
54 # Custom field for the project issues
54 # Custom field for the project issues
55 has_and_belongs_to_many :issue_custom_fields,
55 has_and_belongs_to_many :issue_custom_fields,
56 :class_name => 'IssueCustomField',
56 :class_name => 'IssueCustomField',
57 :order => "#{CustomField.table_name}.position",
57 :order => "#{CustomField.table_name}.position",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 :association_foreign_key => 'custom_field_id'
59 :association_foreign_key => 'custom_field_id'
60
60
61 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :delete_permission => :manage_files
63 :delete_permission => :manage_files
64
64
65 acts_as_customizable
65 acts_as_customizable
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :author => nil
69 :author => nil
70
70
71 attr_protected :status
71 attr_protected :status
72
72
73 validates_presence_of :name, :identifier
73 validates_presence_of :name, :identifier
74 validates_uniqueness_of :identifier
74 validates_uniqueness_of :identifier
75 validates_associated :repository, :wiki
75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only
79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
80 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words
81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
83
83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 before_destroy :delete_all_members
85 before_destroy :delete_all_members
86
86
87 scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
87 scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
88 scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
88 scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
89 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
89 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
90 scope :all_public, { :conditions => { :is_public => true } }
90 scope :all_public, { :conditions => { :is_public => true } }
91 scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
91 scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
92 scope :allowed_to, lambda {|*args|
92 scope :allowed_to, lambda {|*args|
93 user = User.current
93 user = User.current
94 permission = nil
94 permission = nil
95 if args.first.is_a?(Symbol)
95 if args.first.is_a?(Symbol)
96 permission = args.shift
96 permission = args.shift
97 else
97 else
98 user = args.shift
98 user = args.shift
99 permission = args.shift
99 permission = args.shift
100 end
100 end
101 { :conditions => Project.allowed_to_condition(user, permission, *args) }
101 { :conditions => Project.allowed_to_condition(user, permission, *args) }
102 }
102 }
103 scope :like, lambda {|arg|
103 scope :like, lambda {|arg|
104 if arg.blank?
104 if arg.blank?
105 {}
105 {}
106 else
106 else
107 pattern = "%#{arg.to_s.strip.downcase}%"
107 pattern = "%#{arg.to_s.strip.downcase}%"
108 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
108 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
109 end
109 end
110 }
110 }
111
111
112 def initialize(attributes=nil, *args)
112 def initialize(attributes=nil, *args)
113 super
113 super
114
114
115 initialized = (attributes || {}).stringify_keys
115 initialized = (attributes || {}).stringify_keys
116 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
116 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
117 self.identifier = Project.next_identifier
117 self.identifier = Project.next_identifier
118 end
118 end
119 if !initialized.key?('is_public')
119 if !initialized.key?('is_public')
120 self.is_public = Setting.default_projects_public?
120 self.is_public = Setting.default_projects_public?
121 end
121 end
122 if !initialized.key?('enabled_module_names')
122 if !initialized.key?('enabled_module_names')
123 self.enabled_module_names = Setting.default_projects_modules
123 self.enabled_module_names = Setting.default_projects_modules
124 end
124 end
125 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
125 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
126 self.trackers = Tracker.sorted.all
126 self.trackers = Tracker.sorted.all
127 end
127 end
128 end
128 end
129
129
130 def identifier=(identifier)
130 def identifier=(identifier)
131 super unless identifier_frozen?
131 super unless identifier_frozen?
132 end
132 end
133
133
134 def identifier_frozen?
134 def identifier_frozen?
135 errors[:identifier].blank? && !(new_record? || identifier.blank?)
135 errors[:identifier].blank? && !(new_record? || identifier.blank?)
136 end
136 end
137
137
138 # returns latest created projects
138 # returns latest created projects
139 # non public projects will be returned only if user is a member of those
139 # non public projects will be returned only if user is a member of those
140 def self.latest(user=nil, count=5)
140 def self.latest(user=nil, count=5)
141 visible(user).find(:all, :limit => count, :order => "created_on DESC")
141 visible(user).limit(count).order("created_on DESC").all
142 end
142 end
143
143
144 # Returns true if the project is visible to +user+ or to the current user.
144 # Returns true if the project is visible to +user+ or to the current user.
145 def visible?(user=User.current)
145 def visible?(user=User.current)
146 user.allowed_to?(:view_project, self)
146 user.allowed_to?(:view_project, self)
147 end
147 end
148
148
149 # Returns a SQL conditions string used to find all projects visible by the specified user.
149 # Returns a SQL conditions string used to find all projects visible by the specified user.
150 #
150 #
151 # Examples:
151 # Examples:
152 # Project.visible_condition(admin) => "projects.status = 1"
152 # Project.visible_condition(admin) => "projects.status = 1"
153 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
153 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
154 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
154 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
155 def self.visible_condition(user, options={})
155 def self.visible_condition(user, options={})
156 allowed_to_condition(user, :view_project, options)
156 allowed_to_condition(user, :view_project, options)
157 end
157 end
158
158
159 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
159 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
160 #
160 #
161 # Valid options:
161 # Valid options:
162 # * :project => limit the condition to project
162 # * :project => limit the condition to project
163 # * :with_subprojects => limit the condition to project and its subprojects
163 # * :with_subprojects => limit the condition to project and its subprojects
164 # * :member => limit the condition to the user projects
164 # * :member => limit the condition to the user projects
165 def self.allowed_to_condition(user, permission, options={})
165 def self.allowed_to_condition(user, permission, options={})
166 perm = Redmine::AccessControl.permission(permission)
166 perm = Redmine::AccessControl.permission(permission)
167 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
167 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
168 if perm && perm.project_module
168 if perm && perm.project_module
169 # If the permission belongs to a project module, make sure the module is enabled
169 # If the permission belongs to a project module, make sure the module is enabled
170 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
170 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
171 end
171 end
172 if options[:project]
172 if options[:project]
173 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
173 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
174 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
174 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
175 base_statement = "(#{project_statement}) AND (#{base_statement})"
175 base_statement = "(#{project_statement}) AND (#{base_statement})"
176 end
176 end
177
177
178 if user.admin?
178 if user.admin?
179 base_statement
179 base_statement
180 else
180 else
181 statement_by_role = {}
181 statement_by_role = {}
182 unless options[:member]
182 unless options[:member]
183 role = user.logged? ? Role.non_member : Role.anonymous
183 role = user.logged? ? Role.non_member : Role.anonymous
184 if role.allowed_to?(permission)
184 if role.allowed_to?(permission)
185 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
185 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
186 end
186 end
187 end
187 end
188 if user.logged?
188 if user.logged?
189 user.projects_by_role.each do |role, projects|
189 user.projects_by_role.each do |role, projects|
190 if role.allowed_to?(permission) && projects.any?
190 if role.allowed_to?(permission) && projects.any?
191 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
191 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
192 end
192 end
193 end
193 end
194 end
194 end
195 if statement_by_role.empty?
195 if statement_by_role.empty?
196 "1=0"
196 "1=0"
197 else
197 else
198 if block_given?
198 if block_given?
199 statement_by_role.each do |role, statement|
199 statement_by_role.each do |role, statement|
200 if s = yield(role, user)
200 if s = yield(role, user)
201 statement_by_role[role] = "(#{statement} AND (#{s}))"
201 statement_by_role[role] = "(#{statement} AND (#{s}))"
202 end
202 end
203 end
203 end
204 end
204 end
205 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
205 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
206 end
206 end
207 end
207 end
208 end
208 end
209
209
210 # Returns the Systemwide and project specific activities
210 # Returns the Systemwide and project specific activities
211 def activities(include_inactive=false)
211 def activities(include_inactive=false)
212 if include_inactive
212 if include_inactive
213 return all_activities
213 return all_activities
214 else
214 else
215 return active_activities
215 return active_activities
216 end
216 end
217 end
217 end
218
218
219 # Will create a new Project specific Activity or update an existing one
219 # Will create a new Project specific Activity or update an existing one
220 #
220 #
221 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
221 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
222 # does not successfully save.
222 # does not successfully save.
223 def update_or_create_time_entry_activity(id, activity_hash)
223 def update_or_create_time_entry_activity(id, activity_hash)
224 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
224 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
225 self.create_time_entry_activity_if_needed(activity_hash)
225 self.create_time_entry_activity_if_needed(activity_hash)
226 else
226 else
227 activity = project.time_entry_activities.find_by_id(id.to_i)
227 activity = project.time_entry_activities.find_by_id(id.to_i)
228 activity.update_attributes(activity_hash) if activity
228 activity.update_attributes(activity_hash) if activity
229 end
229 end
230 end
230 end
231
231
232 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
232 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
233 #
233 #
234 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
234 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
235 # does not successfully save.
235 # does not successfully save.
236 def create_time_entry_activity_if_needed(activity)
236 def create_time_entry_activity_if_needed(activity)
237 if activity['parent_id']
237 if activity['parent_id']
238
238
239 parent_activity = TimeEntryActivity.find(activity['parent_id'])
239 parent_activity = TimeEntryActivity.find(activity['parent_id'])
240 activity['name'] = parent_activity.name
240 activity['name'] = parent_activity.name
241 activity['position'] = parent_activity.position
241 activity['position'] = parent_activity.position
242
242
243 if Enumeration.overridding_change?(activity, parent_activity)
243 if Enumeration.overridding_change?(activity, parent_activity)
244 project_activity = self.time_entry_activities.create(activity)
244 project_activity = self.time_entry_activities.create(activity)
245
245
246 if project_activity.new_record?
246 if project_activity.new_record?
247 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
247 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
248 else
248 else
249 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
249 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
250 end
250 end
251 end
251 end
252 end
252 end
253 end
253 end
254
254
255 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
255 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
256 #
256 #
257 # Examples:
257 # Examples:
258 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
258 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
259 # project.project_condition(false) => "projects.id = 1"
259 # project.project_condition(false) => "projects.id = 1"
260 def project_condition(with_subprojects)
260 def project_condition(with_subprojects)
261 cond = "#{Project.table_name}.id = #{id}"
261 cond = "#{Project.table_name}.id = #{id}"
262 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
262 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
263 cond
263 cond
264 end
264 end
265
265
266 def self.find(*args)
266 def self.find(*args)
267 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
267 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
268 project = find_by_identifier(*args)
268 project = find_by_identifier(*args)
269 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
269 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
270 project
270 project
271 else
271 else
272 super
272 super
273 end
273 end
274 end
274 end
275
275
276 def self.find_by_param(*args)
276 def self.find_by_param(*args)
277 self.find(*args)
277 self.find(*args)
278 end
278 end
279
279
280 def reload(*args)
280 def reload(*args)
281 @shared_versions = nil
281 @shared_versions = nil
282 @rolled_up_versions = nil
282 @rolled_up_versions = nil
283 @rolled_up_trackers = nil
283 @rolled_up_trackers = nil
284 @all_issue_custom_fields = nil
284 @all_issue_custom_fields = nil
285 @all_time_entry_custom_fields = nil
285 @all_time_entry_custom_fields = nil
286 @to_param = nil
286 @to_param = nil
287 @allowed_parents = nil
287 @allowed_parents = nil
288 @allowed_permissions = nil
288 @allowed_permissions = nil
289 @actions_allowed = nil
289 @actions_allowed = nil
290 super
290 super
291 end
291 end
292
292
293 def to_param
293 def to_param
294 # id is used for projects with a numeric identifier (compatibility)
294 # id is used for projects with a numeric identifier (compatibility)
295 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
295 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
296 end
296 end
297
297
298 def active?
298 def active?
299 self.status == STATUS_ACTIVE
299 self.status == STATUS_ACTIVE
300 end
300 end
301
301
302 def archived?
302 def archived?
303 self.status == STATUS_ARCHIVED
303 self.status == STATUS_ARCHIVED
304 end
304 end
305
305
306 # Archives the project and its descendants
306 # Archives the project and its descendants
307 def archive
307 def archive
308 # Check that there is no issue of a non descendant project that is assigned
308 # Check that there is no issue of a non descendant project that is assigned
309 # to one of the project or descendant versions
309 # to one of the project or descendant versions
310 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
310 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
311 if v_ids.any? && Issue.find(:first, :include => :project,
311 if v_ids.any? && Issue.find(:first, :include => :project,
312 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
312 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
313 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
313 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
314 return false
314 return false
315 end
315 end
316 Project.transaction do
316 Project.transaction do
317 archive!
317 archive!
318 end
318 end
319 true
319 true
320 end
320 end
321
321
322 # Unarchives the project
322 # Unarchives the project
323 # All its ancestors must be active
323 # All its ancestors must be active
324 def unarchive
324 def unarchive
325 return false if ancestors.detect {|a| !a.active?}
325 return false if ancestors.detect {|a| !a.active?}
326 update_attribute :status, STATUS_ACTIVE
326 update_attribute :status, STATUS_ACTIVE
327 end
327 end
328
328
329 def close
329 def close
330 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
330 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
331 end
331 end
332
332
333 def reopen
333 def reopen
334 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
334 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
335 end
335 end
336
336
337 # Returns an array of projects the project can be moved to
337 # Returns an array of projects the project can be moved to
338 # by the current user
338 # by the current user
339 def allowed_parents
339 def allowed_parents
340 return @allowed_parents if @allowed_parents
340 return @allowed_parents if @allowed_parents
341 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
341 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
342 @allowed_parents = @allowed_parents - self_and_descendants
342 @allowed_parents = @allowed_parents - self_and_descendants
343 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
343 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
344 @allowed_parents << nil
344 @allowed_parents << nil
345 end
345 end
346 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
346 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
347 @allowed_parents << parent
347 @allowed_parents << parent
348 end
348 end
349 @allowed_parents
349 @allowed_parents
350 end
350 end
351
351
352 # Sets the parent of the project with authorization check
352 # Sets the parent of the project with authorization check
353 def set_allowed_parent!(p)
353 def set_allowed_parent!(p)
354 unless p.nil? || p.is_a?(Project)
354 unless p.nil? || p.is_a?(Project)
355 if p.to_s.blank?
355 if p.to_s.blank?
356 p = nil
356 p = nil
357 else
357 else
358 p = Project.find_by_id(p)
358 p = Project.find_by_id(p)
359 return false unless p
359 return false unless p
360 end
360 end
361 end
361 end
362 if p.nil?
362 if p.nil?
363 if !new_record? && allowed_parents.empty?
363 if !new_record? && allowed_parents.empty?
364 return false
364 return false
365 end
365 end
366 elsif !allowed_parents.include?(p)
366 elsif !allowed_parents.include?(p)
367 return false
367 return false
368 end
368 end
369 set_parent!(p)
369 set_parent!(p)
370 end
370 end
371
371
372 # Sets the parent of the project
372 # Sets the parent of the project
373 # Argument can be either a Project, a String, a Fixnum or nil
373 # Argument can be either a Project, a String, a Fixnum or nil
374 def set_parent!(p)
374 def set_parent!(p)
375 unless p.nil? || p.is_a?(Project)
375 unless p.nil? || p.is_a?(Project)
376 if p.to_s.blank?
376 if p.to_s.blank?
377 p = nil
377 p = nil
378 else
378 else
379 p = Project.find_by_id(p)
379 p = Project.find_by_id(p)
380 return false unless p
380 return false unless p
381 end
381 end
382 end
382 end
383 if p == parent && !p.nil?
383 if p == parent && !p.nil?
384 # Nothing to do
384 # Nothing to do
385 true
385 true
386 elsif p.nil? || (p.active? && move_possible?(p))
386 elsif p.nil? || (p.active? && move_possible?(p))
387 set_or_update_position_under(p)
387 set_or_update_position_under(p)
388 Issue.update_versions_from_hierarchy_change(self)
388 Issue.update_versions_from_hierarchy_change(self)
389 true
389 true
390 else
390 else
391 # Can not move to the given target
391 # Can not move to the given target
392 false
392 false
393 end
393 end
394 end
394 end
395
395
396 # Recalculates all lft and rgt values based on project names
396 # Recalculates all lft and rgt values based on project names
397 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
397 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
398 # Used in BuildProjectsTree migration
398 # Used in BuildProjectsTree migration
399 def self.rebuild_tree!
399 def self.rebuild_tree!
400 transaction do
400 transaction do
401 update_all "lft = NULL, rgt = NULL"
401 update_all "lft = NULL, rgt = NULL"
402 rebuild!(false)
402 rebuild!(false)
403 end
403 end
404 end
404 end
405
405
406 # Returns an array of the trackers used by the project and its active sub projects
406 # Returns an array of the trackers used by the project and its active sub projects
407 def rolled_up_trackers
407 def rolled_up_trackers
408 @rolled_up_trackers ||=
408 @rolled_up_trackers ||=
409 Tracker.find(:all, :joins => :projects,
409 Tracker.find(:all, :joins => :projects,
410 :select => "DISTINCT #{Tracker.table_name}.*",
410 :select => "DISTINCT #{Tracker.table_name}.*",
411 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
411 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt],
412 :order => "#{Tracker.table_name}.position")
412 :order => "#{Tracker.table_name}.position")
413 end
413 end
414
414
415 # Closes open and locked project versions that are completed
415 # Closes open and locked project versions that are completed
416 def close_completed_versions
416 def close_completed_versions
417 Version.transaction do
417 Version.transaction do
418 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
418 versions.where(:status => %w(open locked)).all.each do |version|
419 if version.completed?
419 if version.completed?
420 version.update_attribute(:status, 'closed')
420 version.update_attribute(:status, 'closed')
421 end
421 end
422 end
422 end
423 end
423 end
424 end
424 end
425
425
426 # Returns a scope of the Versions on subprojects
426 # Returns a scope of the Versions on subprojects
427 def rolled_up_versions
427 def rolled_up_versions
428 @rolled_up_versions ||=
428 @rolled_up_versions ||=
429 Version.scoped(:include => :project,
429 Version.scoped(:include => :project,
430 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
430 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
431 end
431 end
432
432
433 # Returns a scope of the Versions used by the project
433 # Returns a scope of the Versions used by the project
434 def shared_versions
434 def shared_versions
435 if new_record?
435 if new_record?
436 Version.scoped(:include => :project,
436 Version.scoped(:include => :project,
437 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
437 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
438 else
438 else
439 @shared_versions ||= begin
439 @shared_versions ||= begin
440 r = root? ? self : root
440 r = root? ? self : root
441 Version.scoped(:include => :project,
441 Version.scoped(:include => :project,
442 :conditions => "#{Project.table_name}.id = #{id}" +
442 :conditions => "#{Project.table_name}.id = #{id}" +
443 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
443 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
444 " #{Version.table_name}.sharing = 'system'" +
444 " #{Version.table_name}.sharing = 'system'" +
445 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
445 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
446 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
446 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
447 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
447 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
448 "))")
448 "))")
449 end
449 end
450 end
450 end
451 end
451 end
452
452
453 # Returns a hash of project users grouped by role
453 # Returns a hash of project users grouped by role
454 def users_by_role
454 def users_by_role
455 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
455 members.includes(:user, :roles).all.inject({}) do |h, m|
456 m.roles.each do |r|
456 m.roles.each do |r|
457 h[r] ||= []
457 h[r] ||= []
458 h[r] << m.user
458 h[r] << m.user
459 end
459 end
460 h
460 h
461 end
461 end
462 end
462 end
463
463
464 # Deletes all project's members
464 # Deletes all project's members
465 def delete_all_members
465 def delete_all_members
466 me, mr = Member.table_name, MemberRole.table_name
466 me, mr = Member.table_name, MemberRole.table_name
467 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
467 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
468 Member.delete_all(['project_id = ?', id])
468 Member.delete_all(['project_id = ?', id])
469 end
469 end
470
470
471 # Users/groups issues can be assigned to
471 # Users/groups issues can be assigned to
472 def assignable_users
472 def assignable_users
473 assignable = Setting.issue_group_assignment? ? member_principals : members
473 assignable = Setting.issue_group_assignment? ? member_principals : members
474 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
474 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
475 end
475 end
476
476
477 # Returns the mail adresses of users that should be always notified on project events
477 # Returns the mail adresses of users that should be always notified on project events
478 def recipients
478 def recipients
479 notified_users.collect {|user| user.mail}
479 notified_users.collect {|user| user.mail}
480 end
480 end
481
481
482 # Returns the users that should be notified on project events
482 # Returns the users that should be notified on project events
483 def notified_users
483 def notified_users
484 # TODO: User part should be extracted to User#notify_about?
484 # TODO: User part should be extracted to User#notify_about?
485 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
485 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
486 end
486 end
487
487
488 # Returns an array of all custom fields enabled for project issues
488 # Returns an array of all custom fields enabled for project issues
489 # (explictly associated custom fields and custom fields enabled for all projects)
489 # (explictly associated custom fields and custom fields enabled for all projects)
490 def all_issue_custom_fields
490 def all_issue_custom_fields
491 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
491 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
492 end
492 end
493
493
494 # Returns an array of all custom fields enabled for project time entries
494 # Returns an array of all custom fields enabled for project time entries
495 # (explictly associated custom fields and custom fields enabled for all projects)
495 # (explictly associated custom fields and custom fields enabled for all projects)
496 def all_time_entry_custom_fields
496 def all_time_entry_custom_fields
497 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
497 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
498 end
498 end
499
499
500 def project
500 def project
501 self
501 self
502 end
502 end
503
503
504 def <=>(project)
504 def <=>(project)
505 name.downcase <=> project.name.downcase
505 name.downcase <=> project.name.downcase
506 end
506 end
507
507
508 def to_s
508 def to_s
509 name
509 name
510 end
510 end
511
511
512 # Returns a short description of the projects (first lines)
512 # Returns a short description of the projects (first lines)
513 def short_description(length = 255)
513 def short_description(length = 255)
514 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
514 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
515 end
515 end
516
516
517 def css_classes
517 def css_classes
518 s = 'project'
518 s = 'project'
519 s << ' root' if root?
519 s << ' root' if root?
520 s << ' child' if child?
520 s << ' child' if child?
521 s << (leaf? ? ' leaf' : ' parent')
521 s << (leaf? ? ' leaf' : ' parent')
522 unless active?
522 unless active?
523 if archived?
523 if archived?
524 s << ' archived'
524 s << ' archived'
525 else
525 else
526 s << ' closed'
526 s << ' closed'
527 end
527 end
528 end
528 end
529 s
529 s
530 end
530 end
531
531
532 # The earliest start date of a project, based on it's issues and versions
532 # The earliest start date of a project, based on it's issues and versions
533 def start_date
533 def start_date
534 [
534 [
535 issues.minimum('start_date'),
535 issues.minimum('start_date'),
536 shared_versions.collect(&:effective_date),
536 shared_versions.collect(&:effective_date),
537 shared_versions.collect(&:start_date)
537 shared_versions.collect(&:start_date)
538 ].flatten.compact.min
538 ].flatten.compact.min
539 end
539 end
540
540
541 # The latest due date of an issue or version
541 # The latest due date of an issue or version
542 def due_date
542 def due_date
543 [
543 [
544 issues.maximum('due_date'),
544 issues.maximum('due_date'),
545 shared_versions.collect(&:effective_date),
545 shared_versions.collect(&:effective_date),
546 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
546 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
547 ].flatten.compact.max
547 ].flatten.compact.max
548 end
548 end
549
549
550 def overdue?
550 def overdue?
551 active? && !due_date.nil? && (due_date < Date.today)
551 active? && !due_date.nil? && (due_date < Date.today)
552 end
552 end
553
553
554 # Returns the percent completed for this project, based on the
554 # Returns the percent completed for this project, based on the
555 # progress on it's versions.
555 # progress on it's versions.
556 def completed_percent(options={:include_subprojects => false})
556 def completed_percent(options={:include_subprojects => false})
557 if options.delete(:include_subprojects)
557 if options.delete(:include_subprojects)
558 total = self_and_descendants.collect(&:completed_percent).sum
558 total = self_and_descendants.collect(&:completed_percent).sum
559
559
560 total / self_and_descendants.count
560 total / self_and_descendants.count
561 else
561 else
562 if versions.count > 0
562 if versions.count > 0
563 total = versions.collect(&:completed_pourcent).sum
563 total = versions.collect(&:completed_pourcent).sum
564
564
565 total / versions.count
565 total / versions.count
566 else
566 else
567 100
567 100
568 end
568 end
569 end
569 end
570 end
570 end
571
571
572 # Return true if this project allows to do the specified action.
572 # Return true if this project allows to do the specified action.
573 # action can be:
573 # action can be:
574 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
574 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
575 # * a permission Symbol (eg. :edit_project)
575 # * a permission Symbol (eg. :edit_project)
576 def allows_to?(action)
576 def allows_to?(action)
577 if archived?
577 if archived?
578 # No action allowed on archived projects
578 # No action allowed on archived projects
579 return false
579 return false
580 end
580 end
581 unless active? || Redmine::AccessControl.read_action?(action)
581 unless active? || Redmine::AccessControl.read_action?(action)
582 # No write action allowed on closed projects
582 # No write action allowed on closed projects
583 return false
583 return false
584 end
584 end
585 # No action allowed on disabled modules
585 # No action allowed on disabled modules
586 if action.is_a? Hash
586 if action.is_a? Hash
587 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
587 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
588 else
588 else
589 allowed_permissions.include? action
589 allowed_permissions.include? action
590 end
590 end
591 end
591 end
592
592
593 def module_enabled?(module_name)
593 def module_enabled?(module_name)
594 module_name = module_name.to_s
594 module_name = module_name.to_s
595 enabled_modules.detect {|m| m.name == module_name}
595 enabled_modules.detect {|m| m.name == module_name}
596 end
596 end
597
597
598 def enabled_module_names=(module_names)
598 def enabled_module_names=(module_names)
599 if module_names && module_names.is_a?(Array)
599 if module_names && module_names.is_a?(Array)
600 module_names = module_names.collect(&:to_s).reject(&:blank?)
600 module_names = module_names.collect(&:to_s).reject(&:blank?)
601 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
601 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
602 else
602 else
603 enabled_modules.clear
603 enabled_modules.clear
604 end
604 end
605 end
605 end
606
606
607 # Returns an array of the enabled modules names
607 # Returns an array of the enabled modules names
608 def enabled_module_names
608 def enabled_module_names
609 enabled_modules.collect(&:name)
609 enabled_modules.collect(&:name)
610 end
610 end
611
611
612 # Enable a specific module
612 # Enable a specific module
613 #
613 #
614 # Examples:
614 # Examples:
615 # project.enable_module!(:issue_tracking)
615 # project.enable_module!(:issue_tracking)
616 # project.enable_module!("issue_tracking")
616 # project.enable_module!("issue_tracking")
617 def enable_module!(name)
617 def enable_module!(name)
618 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
618 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
619 end
619 end
620
620
621 # Disable a module if it exists
621 # Disable a module if it exists
622 #
622 #
623 # Examples:
623 # Examples:
624 # project.disable_module!(:issue_tracking)
624 # project.disable_module!(:issue_tracking)
625 # project.disable_module!("issue_tracking")
625 # project.disable_module!("issue_tracking")
626 # project.disable_module!(project.enabled_modules.first)
626 # project.disable_module!(project.enabled_modules.first)
627 def disable_module!(target)
627 def disable_module!(target)
628 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
628 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
629 target.destroy unless target.blank?
629 target.destroy unless target.blank?
630 end
630 end
631
631
632 safe_attributes 'name',
632 safe_attributes 'name',
633 'description',
633 'description',
634 'homepage',
634 'homepage',
635 'is_public',
635 'is_public',
636 'identifier',
636 'identifier',
637 'custom_field_values',
637 'custom_field_values',
638 'custom_fields',
638 'custom_fields',
639 'tracker_ids',
639 'tracker_ids',
640 'issue_custom_field_ids'
640 'issue_custom_field_ids'
641
641
642 safe_attributes 'enabled_module_names',
642 safe_attributes 'enabled_module_names',
643 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
643 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
644
644
645 # Returns an array of projects that are in this project's hierarchy
645 # Returns an array of projects that are in this project's hierarchy
646 #
646 #
647 # Example: parents, children, siblings
647 # Example: parents, children, siblings
648 def hierarchy
648 def hierarchy
649 parents = project.self_and_ancestors || []
649 parents = project.self_and_ancestors || []
650 descendants = project.descendants || []
650 descendants = project.descendants || []
651 project_hierarchy = parents | descendants # Set union
651 project_hierarchy = parents | descendants # Set union
652 end
652 end
653
653
654 # Returns an auto-generated project identifier based on the last identifier used
654 # Returns an auto-generated project identifier based on the last identifier used
655 def self.next_identifier
655 def self.next_identifier
656 p = Project.find(:first, :order => 'created_on DESC')
656 p = Project.find(:first, :order => 'created_on DESC')
657 p.nil? ? nil : p.identifier.to_s.succ
657 p.nil? ? nil : p.identifier.to_s.succ
658 end
658 end
659
659
660 # Copies and saves the Project instance based on the +project+.
660 # Copies and saves the Project instance based on the +project+.
661 # Duplicates the source project's:
661 # Duplicates the source project's:
662 # * Wiki
662 # * Wiki
663 # * Versions
663 # * Versions
664 # * Categories
664 # * Categories
665 # * Issues
665 # * Issues
666 # * Members
666 # * Members
667 # * Queries
667 # * Queries
668 #
668 #
669 # Accepts an +options+ argument to specify what to copy
669 # Accepts an +options+ argument to specify what to copy
670 #
670 #
671 # Examples:
671 # Examples:
672 # project.copy(1) # => copies everything
672 # project.copy(1) # => copies everything
673 # project.copy(1, :only => 'members') # => copies members only
673 # project.copy(1, :only => 'members') # => copies members only
674 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
674 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
675 def copy(project, options={})
675 def copy(project, options={})
676 project = project.is_a?(Project) ? project : Project.find(project)
676 project = project.is_a?(Project) ? project : Project.find(project)
677
677
678 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
678 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
679 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
679 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
680
680
681 Project.transaction do
681 Project.transaction do
682 if save
682 if save
683 reload
683 reload
684 to_be_copied.each do |name|
684 to_be_copied.each do |name|
685 send "copy_#{name}", project
685 send "copy_#{name}", project
686 end
686 end
687 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
687 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
688 save
688 save
689 end
689 end
690 end
690 end
691 end
691 end
692
692
693
693
694 # Copies +project+ and returns the new instance. This will not save
694 # Copies +project+ and returns the new instance. This will not save
695 # the copy
695 # the copy
696 def self.copy_from(project)
696 def self.copy_from(project)
697 begin
697 begin
698 project = project.is_a?(Project) ? project : Project.find(project)
698 project = project.is_a?(Project) ? project : Project.find(project)
699 if project
699 if project
700 # clear unique attributes
700 # clear unique attributes
701 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
701 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
702 copy = Project.new(attributes)
702 copy = Project.new(attributes)
703 copy.enabled_modules = project.enabled_modules
703 copy.enabled_modules = project.enabled_modules
704 copy.trackers = project.trackers
704 copy.trackers = project.trackers
705 copy.custom_values = project.custom_values.collect {|v| v.clone}
705 copy.custom_values = project.custom_values.collect {|v| v.clone}
706 copy.issue_custom_fields = project.issue_custom_fields
706 copy.issue_custom_fields = project.issue_custom_fields
707 return copy
707 return copy
708 else
708 else
709 return nil
709 return nil
710 end
710 end
711 rescue ActiveRecord::RecordNotFound
711 rescue ActiveRecord::RecordNotFound
712 return nil
712 return nil
713 end
713 end
714 end
714 end
715
715
716 # Yields the given block for each project with its level in the tree
716 # Yields the given block for each project with its level in the tree
717 def self.project_tree(projects, &block)
717 def self.project_tree(projects, &block)
718 ancestors = []
718 ancestors = []
719 projects.sort_by(&:lft).each do |project|
719 projects.sort_by(&:lft).each do |project|
720 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
720 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
721 ancestors.pop
721 ancestors.pop
722 end
722 end
723 yield project, ancestors.size
723 yield project, ancestors.size
724 ancestors << project
724 ancestors << project
725 end
725 end
726 end
726 end
727
727
728 private
728 private
729
729
730 # Copies wiki from +project+
730 # Copies wiki from +project+
731 def copy_wiki(project)
731 def copy_wiki(project)
732 # Check that the source project has a wiki first
732 # Check that the source project has a wiki first
733 unless project.wiki.nil?
733 unless project.wiki.nil?
734 self.wiki ||= Wiki.new
734 self.wiki ||= Wiki.new
735 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
735 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
736 wiki_pages_map = {}
736 wiki_pages_map = {}
737 project.wiki.pages.each do |page|
737 project.wiki.pages.each do |page|
738 # Skip pages without content
738 # Skip pages without content
739 next if page.content.nil?
739 next if page.content.nil?
740 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
740 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
741 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
741 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
742 new_wiki_page.content = new_wiki_content
742 new_wiki_page.content = new_wiki_content
743 wiki.pages << new_wiki_page
743 wiki.pages << new_wiki_page
744 wiki_pages_map[page.id] = new_wiki_page
744 wiki_pages_map[page.id] = new_wiki_page
745 end
745 end
746 wiki.save
746 wiki.save
747 # Reproduce page hierarchy
747 # Reproduce page hierarchy
748 project.wiki.pages.each do |page|
748 project.wiki.pages.each do |page|
749 if page.parent_id && wiki_pages_map[page.id]
749 if page.parent_id && wiki_pages_map[page.id]
750 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
750 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
751 wiki_pages_map[page.id].save
751 wiki_pages_map[page.id].save
752 end
752 end
753 end
753 end
754 end
754 end
755 end
755 end
756
756
757 # Copies versions from +project+
757 # Copies versions from +project+
758 def copy_versions(project)
758 def copy_versions(project)
759 project.versions.each do |version|
759 project.versions.each do |version|
760 new_version = Version.new
760 new_version = Version.new
761 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
761 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
762 self.versions << new_version
762 self.versions << new_version
763 end
763 end
764 end
764 end
765
765
766 # Copies issue categories from +project+
766 # Copies issue categories from +project+
767 def copy_issue_categories(project)
767 def copy_issue_categories(project)
768 project.issue_categories.each do |issue_category|
768 project.issue_categories.each do |issue_category|
769 new_issue_category = IssueCategory.new
769 new_issue_category = IssueCategory.new
770 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
770 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
771 self.issue_categories << new_issue_category
771 self.issue_categories << new_issue_category
772 end
772 end
773 end
773 end
774
774
775 # Copies issues from +project+
775 # Copies issues from +project+
776 def copy_issues(project)
776 def copy_issues(project)
777 # Stores the source issue id as a key and the copied issues as the
777 # Stores the source issue id as a key and the copied issues as the
778 # value. Used to map the two togeather for issue relations.
778 # value. Used to map the two togeather for issue relations.
779 issues_map = {}
779 issues_map = {}
780
780
781 # Store status and reopen locked/closed versions
781 # Store status and reopen locked/closed versions
782 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
782 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
783 version_statuses.each do |version, status|
783 version_statuses.each do |version, status|
784 version.update_attribute :status, 'open'
784 version.update_attribute :status, 'open'
785 end
785 end
786
786
787 # Get issues sorted by root_id, lft so that parent issues
787 # Get issues sorted by root_id, lft so that parent issues
788 # get copied before their children
788 # get copied before their children
789 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
789 project.issues.reorder('root_id, lft').all.each do |issue|
790 new_issue = Issue.new
790 new_issue = Issue.new
791 new_issue.copy_from(issue, :subtasks => false, :link => false)
791 new_issue.copy_from(issue, :subtasks => false, :link => false)
792 new_issue.project = self
792 new_issue.project = self
793 # Reassign fixed_versions by name, since names are unique per project
793 # Reassign fixed_versions by name, since names are unique per project
794 if issue.fixed_version && issue.fixed_version.project == project
794 if issue.fixed_version && issue.fixed_version.project == project
795 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
795 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
796 end
796 end
797 # Reassign the category by name, since names are unique per project
797 # Reassign the category by name, since names are unique per project
798 if issue.category
798 if issue.category
799 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
799 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
800 end
800 end
801 # Parent issue
801 # Parent issue
802 if issue.parent_id
802 if issue.parent_id
803 if copied_parent = issues_map[issue.parent_id]
803 if copied_parent = issues_map[issue.parent_id]
804 new_issue.parent_issue_id = copied_parent.id
804 new_issue.parent_issue_id = copied_parent.id
805 end
805 end
806 end
806 end
807
807
808 self.issues << new_issue
808 self.issues << new_issue
809 if new_issue.new_record?
809 if new_issue.new_record?
810 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
810 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
811 else
811 else
812 issues_map[issue.id] = new_issue unless new_issue.new_record?
812 issues_map[issue.id] = new_issue unless new_issue.new_record?
813 end
813 end
814 end
814 end
815
815
816 # Restore locked/closed version statuses
816 # Restore locked/closed version statuses
817 version_statuses.each do |version, status|
817 version_statuses.each do |version, status|
818 version.update_attribute :status, status
818 version.update_attribute :status, status
819 end
819 end
820
820
821 # Relations after in case issues related each other
821 # Relations after in case issues related each other
822 project.issues.each do |issue|
822 project.issues.each do |issue|
823 new_issue = issues_map[issue.id]
823 new_issue = issues_map[issue.id]
824 unless new_issue
824 unless new_issue
825 # Issue was not copied
825 # Issue was not copied
826 next
826 next
827 end
827 end
828
828
829 # Relations
829 # Relations
830 issue.relations_from.each do |source_relation|
830 issue.relations_from.each do |source_relation|
831 new_issue_relation = IssueRelation.new
831 new_issue_relation = IssueRelation.new
832 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
832 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
833 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
833 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
834 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
834 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
835 new_issue_relation.issue_to = source_relation.issue_to
835 new_issue_relation.issue_to = source_relation.issue_to
836 end
836 end
837 new_issue.relations_from << new_issue_relation
837 new_issue.relations_from << new_issue_relation
838 end
838 end
839
839
840 issue.relations_to.each do |source_relation|
840 issue.relations_to.each do |source_relation|
841 new_issue_relation = IssueRelation.new
841 new_issue_relation = IssueRelation.new
842 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
842 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
843 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
843 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
844 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
844 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
845 new_issue_relation.issue_from = source_relation.issue_from
845 new_issue_relation.issue_from = source_relation.issue_from
846 end
846 end
847 new_issue.relations_to << new_issue_relation
847 new_issue.relations_to << new_issue_relation
848 end
848 end
849 end
849 end
850 end
850 end
851
851
852 # Copies members from +project+
852 # Copies members from +project+
853 def copy_members(project)
853 def copy_members(project)
854 # Copy users first, then groups to handle members with inherited and given roles
854 # Copy users first, then groups to handle members with inherited and given roles
855 members_to_copy = []
855 members_to_copy = []
856 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
856 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
857 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
857 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
858
858
859 members_to_copy.each do |member|
859 members_to_copy.each do |member|
860 new_member = Member.new
860 new_member = Member.new
861 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
861 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
862 # only copy non inherited roles
862 # only copy non inherited roles
863 # inherited roles will be added when copying the group membership
863 # inherited roles will be added when copying the group membership
864 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
864 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
865 next if role_ids.empty?
865 next if role_ids.empty?
866 new_member.role_ids = role_ids
866 new_member.role_ids = role_ids
867 new_member.project = self
867 new_member.project = self
868 self.members << new_member
868 self.members << new_member
869 end
869 end
870 end
870 end
871
871
872 # Copies queries from +project+
872 # Copies queries from +project+
873 def copy_queries(project)
873 def copy_queries(project)
874 project.queries.each do |query|
874 project.queries.each do |query|
875 new_query = ::Query.new
875 new_query = ::Query.new
876 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
876 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
877 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
877 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
878 new_query.project = self
878 new_query.project = self
879 new_query.user_id = query.user_id
879 new_query.user_id = query.user_id
880 self.queries << new_query
880 self.queries << new_query
881 end
881 end
882 end
882 end
883
883
884 # Copies boards from +project+
884 # Copies boards from +project+
885 def copy_boards(project)
885 def copy_boards(project)
886 project.boards.each do |board|
886 project.boards.each do |board|
887 new_board = Board.new
887 new_board = Board.new
888 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
888 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
889 new_board.project = self
889 new_board.project = self
890 self.boards << new_board
890 self.boards << new_board
891 end
891 end
892 end
892 end
893
893
894 def allowed_permissions
894 def allowed_permissions
895 @allowed_permissions ||= begin
895 @allowed_permissions ||= begin
896 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
896 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
897 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
897 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
898 end
898 end
899 end
899 end
900
900
901 def allowed_actions
901 def allowed_actions
902 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
902 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
903 end
903 end
904
904
905 # Returns all the active Systemwide and project specific activities
905 # Returns all the active Systemwide and project specific activities
906 def active_activities
906 def active_activities
907 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
907 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
908
908
909 if overridden_activity_ids.empty?
909 if overridden_activity_ids.empty?
910 return TimeEntryActivity.shared.active
910 return TimeEntryActivity.shared.active
911 else
911 else
912 return system_activities_and_project_overrides
912 return system_activities_and_project_overrides
913 end
913 end
914 end
914 end
915
915
916 # Returns all the Systemwide and project specific activities
916 # Returns all the Systemwide and project specific activities
917 # (inactive and active)
917 # (inactive and active)
918 def all_activities
918 def all_activities
919 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
919 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
920
920
921 if overridden_activity_ids.empty?
921 if overridden_activity_ids.empty?
922 return TimeEntryActivity.shared
922 return TimeEntryActivity.shared
923 else
923 else
924 return system_activities_and_project_overrides(true)
924 return system_activities_and_project_overrides(true)
925 end
925 end
926 end
926 end
927
927
928 # Returns the systemwide active activities merged with the project specific overrides
928 # Returns the systemwide active activities merged with the project specific overrides
929 def system_activities_and_project_overrides(include_inactive=false)
929 def system_activities_and_project_overrides(include_inactive=false)
930 if include_inactive
930 if include_inactive
931 return TimeEntryActivity.shared.
931 return TimeEntryActivity.shared.
932 find(:all,
932 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
933 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
934 self.time_entry_activities
933 self.time_entry_activities
935 else
934 else
936 return TimeEntryActivity.shared.active.
935 return TimeEntryActivity.shared.active.
937 find(:all,
936 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
938 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
939 self.time_entry_activities.active
937 self.time_entry_activities.active
940 end
938 end
941 end
939 end
942
940
943 # Archives subprojects recursively
941 # Archives subprojects recursively
944 def archive!
942 def archive!
945 children.each do |subproject|
943 children.each do |subproject|
946 subproject.send :archive!
944 subproject.send :archive!
947 end
945 end
948 update_attribute :status, STATUS_ARCHIVED
946 update_attribute :status, STATUS_ARCHIVED
949 end
947 end
950
948
951 def update_position_under_parent
949 def update_position_under_parent
952 set_or_update_position_under(parent)
950 set_or_update_position_under(parent)
953 end
951 end
954
952
955 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
953 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
956 def set_or_update_position_under(target_parent)
954 def set_or_update_position_under(target_parent)
957 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
955 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
958 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
956 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
959
957
960 if to_be_inserted_before
958 if to_be_inserted_before
961 move_to_left_of(to_be_inserted_before)
959 move_to_left_of(to_be_inserted_before)
962 elsif target_parent.nil?
960 elsif target_parent.nil?
963 if sibs.empty?
961 if sibs.empty?
964 # move_to_root adds the project in first (ie. left) position
962 # move_to_root adds the project in first (ie. left) position
965 move_to_root
963 move_to_root
966 else
964 else
967 move_to_right_of(sibs.last) unless self == sibs.last
965 move_to_right_of(sibs.last) unless self == sibs.last
968 end
966 end
969 else
967 else
970 # move_to_child_of adds the project in last (ie.right) position
968 # move_to_child_of adds the project in last (ie.right) position
971 move_to_child_of(target_parent)
969 move_to_child_of(target_parent)
972 end
970 end
973 end
971 end
974 end
972 end
@@ -1,1084 +1,1079
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
26 if groupable == true
26 if groupable == true
27 self.groupable = name.to_s
27 self.groupable = name.to_s
28 end
28 end
29 self.default_order = options[:default_order]
29 self.default_order = options[:default_order]
30 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !@sortable.nil?
39 !@sortable.nil?
40 end
40 end
41
41
42 def sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 self.groupable = custom_field.group_statement || false
60 self.groupable = custom_field.group_statement || false
61 @cf = custom_field
61 @cf = custom_field
62 end
62 end
63
63
64 def caption
64 def caption
65 @cf.name
65 @cf.name
66 end
66 end
67
67
68 def custom_field
68 def custom_field
69 @cf
69 @cf
70 end
70 end
71
71
72 def value(issue)
72 def value(issue)
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
74 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
75 end
75 end
76
76
77 def css_classes
77 def css_classes
78 @css_classes ||= "#{name} #{@cf.field_format}"
78 @css_classes ||= "#{name} #{@cf.field_format}"
79 end
79 end
80 end
80 end
81
81
82 class Query < ActiveRecord::Base
82 class Query < ActiveRecord::Base
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 end
84 end
85
85
86 belongs_to :project
86 belongs_to :project
87 belongs_to :user
87 belongs_to :user
88 serialize :filters
88 serialize :filters
89 serialize :column_names
89 serialize :column_names
90 serialize :sort_criteria, Array
90 serialize :sort_criteria, Array
91
91
92 attr_protected :project_id, :user_id
92 attr_protected :project_id, :user_id
93
93
94 validates_presence_of :name
94 validates_presence_of :name
95 validates_length_of :name, :maximum => 255
95 validates_length_of :name, :maximum => 255
96 validate :validate_query_filters
96 validate :validate_query_filters
97
97
98 @@operators = { "=" => :label_equals,
98 @@operators = { "=" => :label_equals,
99 "!" => :label_not_equals,
99 "!" => :label_not_equals,
100 "o" => :label_open_issues,
100 "o" => :label_open_issues,
101 "c" => :label_closed_issues,
101 "c" => :label_closed_issues,
102 "!*" => :label_none,
102 "!*" => :label_none,
103 "*" => :label_any,
103 "*" => :label_any,
104 ">=" => :label_greater_or_equal,
104 ">=" => :label_greater_or_equal,
105 "<=" => :label_less_or_equal,
105 "<=" => :label_less_or_equal,
106 "><" => :label_between,
106 "><" => :label_between,
107 "<t+" => :label_in_less_than,
107 "<t+" => :label_in_less_than,
108 ">t+" => :label_in_more_than,
108 ">t+" => :label_in_more_than,
109 "><t+"=> :label_in_the_next_days,
109 "><t+"=> :label_in_the_next_days,
110 "t+" => :label_in,
110 "t+" => :label_in,
111 "t" => :label_today,
111 "t" => :label_today,
112 "w" => :label_this_week,
112 "w" => :label_this_week,
113 ">t-" => :label_less_than_ago,
113 ">t-" => :label_less_than_ago,
114 "<t-" => :label_more_than_ago,
114 "<t-" => :label_more_than_ago,
115 "><t-"=> :label_in_the_past_days,
115 "><t-"=> :label_in_the_past_days,
116 "t-" => :label_ago,
116 "t-" => :label_ago,
117 "~" => :label_contains,
117 "~" => :label_contains,
118 "!~" => :label_not_contains,
118 "!~" => :label_not_contains,
119 "=p" => :label_any_issues_in_project,
119 "=p" => :label_any_issues_in_project,
120 "=!p" => :label_any_issues_not_in_project,
120 "=!p" => :label_any_issues_not_in_project,
121 "!p" => :label_no_issues_in_project}
121 "!p" => :label_no_issues_in_project}
122
122
123 cattr_reader :operators
123 cattr_reader :operators
124
124
125 @@operators_by_filter_type = { :list => [ "=", "!" ],
125 @@operators_by_filter_type = { :list => [ "=", "!" ],
126 :list_status => [ "o", "=", "!", "c", "*" ],
126 :list_status => [ "o", "=", "!", "c", "*" ],
127 :list_optional => [ "=", "!", "!*", "*" ],
127 :list_optional => [ "=", "!", "!*", "*" ],
128 :list_subprojects => [ "*", "!*", "=" ],
128 :list_subprojects => [ "*", "!*", "=" ],
129 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
129 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
130 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
130 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
131 :string => [ "=", "~", "!", "!~", "!*", "*" ],
131 :string => [ "=", "~", "!", "!~", "!*", "*" ],
132 :text => [ "~", "!~", "!*", "*" ],
132 :text => [ "~", "!~", "!*", "*" ],
133 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
133 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
134 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
134 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
135 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
135 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
136
136
137 cattr_reader :operators_by_filter_type
137 cattr_reader :operators_by_filter_type
138
138
139 @@available_columns = [
139 @@available_columns = [
140 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
140 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
141 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
141 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
142 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
142 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
143 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
143 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
144 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
144 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
145 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
145 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
146 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
146 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
147 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
147 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
148 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
148 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
149 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
149 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
150 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
150 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
151 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
151 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
152 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
152 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
153 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
153 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
154 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
154 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
155 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
155 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
156 QueryColumn.new(:relations, :caption => :label_related_issues)
156 QueryColumn.new(:relations, :caption => :label_related_issues)
157 ]
157 ]
158 cattr_reader :available_columns
158 cattr_reader :available_columns
159
159
160 scope :visible, lambda {|*args|
160 scope :visible, lambda {|*args|
161 user = args.shift || User.current
161 user = args.shift || User.current
162 base = Project.allowed_to_condition(user, :view_issues, *args)
162 base = Project.allowed_to_condition(user, :view_issues, *args)
163 user_id = user.logged? ? user.id : 0
163 user_id = user.logged? ? user.id : 0
164 {
164 {
165 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
165 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
166 :include => :project
166 :include => :project
167 }
167 }
168 }
168 }
169
169
170 def initialize(attributes=nil, *args)
170 def initialize(attributes=nil, *args)
171 super attributes
171 super attributes
172 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
172 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
173 @is_for_all = project.nil?
173 @is_for_all = project.nil?
174 end
174 end
175
175
176 def validate_query_filters
176 def validate_query_filters
177 filters.each_key do |field|
177 filters.each_key do |field|
178 if values_for(field)
178 if values_for(field)
179 case type_for(field)
179 case type_for(field)
180 when :integer
180 when :integer
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
181 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
182 when :float
182 when :float
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
183 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
184 when :date, :date_past
184 when :date, :date_past
185 case operator_for(field)
185 case operator_for(field)
186 when "=", ">=", "<=", "><"
186 when "=", ">=", "<=", "><"
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
187 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
188 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
189 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
190 end
190 end
191 end
191 end
192 end
192 end
193
193
194 add_filter_error(field, :blank) unless
194 add_filter_error(field, :blank) unless
195 # filter requires one or more values
195 # filter requires one or more values
196 (values_for(field) and !values_for(field).first.blank?) or
196 (values_for(field) and !values_for(field).first.blank?) or
197 # filter doesn't require any value
197 # filter doesn't require any value
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
199 end if filters
199 end if filters
200 end
200 end
201
201
202 def add_filter_error(field, message)
202 def add_filter_error(field, message)
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
203 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
204 errors.add(:base, m)
204 errors.add(:base, m)
205 end
205 end
206
206
207 # Returns true if the query is visible to +user+ or the current user.
207 # Returns true if the query is visible to +user+ or the current user.
208 def visible?(user=User.current)
208 def visible?(user=User.current)
209 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
209 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
210 end
210 end
211
211
212 def editable_by?(user)
212 def editable_by?(user)
213 return false unless user
213 return false unless user
214 # Admin can edit them all and regular users can edit their private queries
214 # Admin can edit them all and regular users can edit their private queries
215 return true if user.admin? || (!is_public && self.user_id == user.id)
215 return true if user.admin? || (!is_public && self.user_id == user.id)
216 # Members can not edit public queries that are for all project (only admin is allowed to)
216 # Members can not edit public queries that are for all project (only admin is allowed to)
217 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
217 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
218 end
218 end
219
219
220 def trackers
220 def trackers
221 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
221 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
222 end
222 end
223
223
224 # Returns a hash of localized labels for all filter operators
224 # Returns a hash of localized labels for all filter operators
225 def self.operators_labels
225 def self.operators_labels
226 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
226 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
227 end
227 end
228
228
229 def available_filters
229 def available_filters
230 return @available_filters if @available_filters
230 return @available_filters if @available_filters
231 @available_filters = {
231 @available_filters = {
232 "status_id" => {
232 "status_id" => {
233 :type => :list_status, :order => 0,
233 :type => :list_status, :order => 0,
234 :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }
234 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
235 },
235 },
236 "tracker_id" => {
236 "tracker_id" => {
237 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
237 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
238 },
238 },
239 "priority_id" => {
239 "priority_id" => {
240 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
240 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
241 },
241 },
242 "subject" => { :type => :text, :order => 8 },
242 "subject" => { :type => :text, :order => 8 },
243 "created_on" => { :type => :date_past, :order => 9 },
243 "created_on" => { :type => :date_past, :order => 9 },
244 "updated_on" => { :type => :date_past, :order => 10 },
244 "updated_on" => { :type => :date_past, :order => 10 },
245 "start_date" => { :type => :date, :order => 11 },
245 "start_date" => { :type => :date, :order => 11 },
246 "due_date" => { :type => :date, :order => 12 },
246 "due_date" => { :type => :date, :order => 12 },
247 "estimated_hours" => { :type => :float, :order => 13 },
247 "estimated_hours" => { :type => :float, :order => 13 },
248 "done_ratio" => { :type => :integer, :order => 14 }
248 "done_ratio" => { :type => :integer, :order => 14 }
249 }
249 }
250 IssueRelation::TYPES.each do |relation_type, options|
250 IssueRelation::TYPES.each do |relation_type, options|
251 @available_filters[relation_type] = {
251 @available_filters[relation_type] = {
252 :type => :relation, :order => @available_filters.size + 100,
252 :type => :relation, :order => @available_filters.size + 100,
253 :label => options[:name]
253 :label => options[:name]
254 }
254 }
255 end
255 end
256 principals = []
256 principals = []
257 if project
257 if project
258 principals += project.principals.sort
258 principals += project.principals.sort
259 unless project.leaf?
259 unless project.leaf?
260 subprojects = project.descendants.visible.all
260 subprojects = project.descendants.visible.all
261 if subprojects.any?
261 if subprojects.any?
262 @available_filters["subproject_id"] = {
262 @available_filters["subproject_id"] = {
263 :type => :list_subprojects, :order => 13,
263 :type => :list_subprojects, :order => 13,
264 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
264 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
265 }
265 }
266 principals += Principal.member_of(subprojects)
266 principals += Principal.member_of(subprojects)
267 end
267 end
268 end
268 end
269 else
269 else
270 if all_projects.any?
270 if all_projects.any?
271 # members of visible projects
271 # members of visible projects
272 principals += Principal.member_of(all_projects)
272 principals += Principal.member_of(all_projects)
273 # project filter
273 # project filter
274 project_values = []
274 project_values = []
275 if User.current.logged? && User.current.memberships.any?
275 if User.current.logged? && User.current.memberships.any?
276 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
276 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
277 end
277 end
278 project_values += all_projects_values
278 project_values += all_projects_values
279 @available_filters["project_id"] = {
279 @available_filters["project_id"] = {
280 :type => :list, :order => 1, :values => project_values
280 :type => :list, :order => 1, :values => project_values
281 } unless project_values.empty?
281 } unless project_values.empty?
282 end
282 end
283 end
283 end
284 principals.uniq!
284 principals.uniq!
285 principals.sort!
285 principals.sort!
286 users = principals.select {|p| p.is_a?(User)}
286 users = principals.select {|p| p.is_a?(User)}
287
287
288 assigned_to_values = []
288 assigned_to_values = []
289 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
289 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
290 assigned_to_values += (Setting.issue_group_assignment? ?
290 assigned_to_values += (Setting.issue_group_assignment? ?
291 principals : users).collect{|s| [s.name, s.id.to_s] }
291 principals : users).collect{|s| [s.name, s.id.to_s] }
292 @available_filters["assigned_to_id"] = {
292 @available_filters["assigned_to_id"] = {
293 :type => :list_optional, :order => 4, :values => assigned_to_values
293 :type => :list_optional, :order => 4, :values => assigned_to_values
294 } unless assigned_to_values.empty?
294 } unless assigned_to_values.empty?
295
295
296 author_values = []
296 author_values = []
297 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
297 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
298 author_values += users.collect{|s| [s.name, s.id.to_s] }
298 author_values += users.collect{|s| [s.name, s.id.to_s] }
299 @available_filters["author_id"] = {
299 @available_filters["author_id"] = {
300 :type => :list, :order => 5, :values => author_values
300 :type => :list, :order => 5, :values => author_values
301 } unless author_values.empty?
301 } unless author_values.empty?
302
302
303 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
303 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
304 @available_filters["member_of_group"] = {
304 @available_filters["member_of_group"] = {
305 :type => :list_optional, :order => 6, :values => group_values
305 :type => :list_optional, :order => 6, :values => group_values
306 } unless group_values.empty?
306 } unless group_values.empty?
307
307
308 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
308 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
309 @available_filters["assigned_to_role"] = {
309 @available_filters["assigned_to_role"] = {
310 :type => :list_optional, :order => 7, :values => role_values
310 :type => :list_optional, :order => 7, :values => role_values
311 } unless role_values.empty?
311 } unless role_values.empty?
312
312
313 if User.current.logged?
313 if User.current.logged?
314 @available_filters["watcher_id"] = {
314 @available_filters["watcher_id"] = {
315 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
315 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
316 }
316 }
317 end
317 end
318
318
319 if project
319 if project
320 # project specific filters
320 # project specific filters
321 categories = project.issue_categories.all
321 categories = project.issue_categories.all
322 unless categories.empty?
322 unless categories.empty?
323 @available_filters["category_id"] = {
323 @available_filters["category_id"] = {
324 :type => :list_optional, :order => 6,
324 :type => :list_optional, :order => 6,
325 :values => categories.collect{|s| [s.name, s.id.to_s] }
325 :values => categories.collect{|s| [s.name, s.id.to_s] }
326 }
326 }
327 end
327 end
328 versions = project.shared_versions.all
328 versions = project.shared_versions.all
329 unless versions.empty?
329 unless versions.empty?
330 @available_filters["fixed_version_id"] = {
330 @available_filters["fixed_version_id"] = {
331 :type => :list_optional, :order => 7,
331 :type => :list_optional, :order => 7,
332 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
332 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
333 }
333 }
334 end
334 end
335 add_custom_fields_filters(project.all_issue_custom_fields)
335 add_custom_fields_filters(project.all_issue_custom_fields)
336 else
336 else
337 # global filters for cross project issue list
337 # global filters for cross project issue list
338 system_shared_versions = Version.visible.find_all_by_sharing('system')
338 system_shared_versions = Version.visible.find_all_by_sharing('system')
339 unless system_shared_versions.empty?
339 unless system_shared_versions.empty?
340 @available_filters["fixed_version_id"] = {
340 @available_filters["fixed_version_id"] = {
341 :type => :list_optional, :order => 7,
341 :type => :list_optional, :order => 7,
342 :values => system_shared_versions.sort.collect{|s|
342 :values => system_shared_versions.sort.collect{|s|
343 ["#{s.project.name} - #{s.name}", s.id.to_s]
343 ["#{s.project.name} - #{s.name}", s.id.to_s]
344 }
344 }
345 }
345 }
346 end
346 end
347 add_custom_fields_filters(
347 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
348 IssueCustomField.find(:all,
349 :conditions => {
350 :is_filter => true,
351 :is_for_all => true
352 }))
353 end
348 end
354 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
349 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
355 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
350 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
356 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
351 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
357 @available_filters["is_private"] = {
352 @available_filters["is_private"] = {
358 :type => :list, :order => 16,
353 :type => :list, :order => 16,
359 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
354 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
360 }
355 }
361 end
356 end
362 Tracker.disabled_core_fields(trackers).each {|field|
357 Tracker.disabled_core_fields(trackers).each {|field|
363 @available_filters.delete field
358 @available_filters.delete field
364 }
359 }
365 @available_filters.each do |field, options|
360 @available_filters.each do |field, options|
366 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
361 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
367 end
362 end
368 @available_filters
363 @available_filters
369 end
364 end
370
365
371 # Returns a representation of the available filters for JSON serialization
366 # Returns a representation of the available filters for JSON serialization
372 def available_filters_as_json
367 def available_filters_as_json
373 json = {}
368 json = {}
374 available_filters.each do |field, options|
369 available_filters.each do |field, options|
375 json[field] = options.slice(:type, :name, :values).stringify_keys
370 json[field] = options.slice(:type, :name, :values).stringify_keys
376 end
371 end
377 json
372 json
378 end
373 end
379
374
380 def all_projects
375 def all_projects
381 @all_projects ||= Project.visible.all
376 @all_projects ||= Project.visible.all
382 end
377 end
383
378
384 def all_projects_values
379 def all_projects_values
385 return @all_projects_values if @all_projects_values
380 return @all_projects_values if @all_projects_values
386
381
387 values = []
382 values = []
388 Project.project_tree(all_projects) do |p, level|
383 Project.project_tree(all_projects) do |p, level|
389 prefix = (level > 0 ? ('--' * level + ' ') : '')
384 prefix = (level > 0 ? ('--' * level + ' ') : '')
390 values << ["#{prefix}#{p.name}", p.id.to_s]
385 values << ["#{prefix}#{p.name}", p.id.to_s]
391 end
386 end
392 @all_projects_values = values
387 @all_projects_values = values
393 end
388 end
394
389
395 def add_filter(field, operator, values)
390 def add_filter(field, operator, values)
396 # values must be an array
391 # values must be an array
397 return unless values.nil? || values.is_a?(Array)
392 return unless values.nil? || values.is_a?(Array)
398 # check if field is defined as an available filter
393 # check if field is defined as an available filter
399 if available_filters.has_key? field
394 if available_filters.has_key? field
400 filter_options = available_filters[field]
395 filter_options = available_filters[field]
401 # check if operator is allowed for that filter
396 # check if operator is allowed for that filter
402 #if @@operators_by_filter_type[filter_options[:type]].include? operator
397 #if @@operators_by_filter_type[filter_options[:type]].include? operator
403 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
398 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
404 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
399 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
405 #end
400 #end
406 filters[field] = {:operator => operator, :values => (values || [''])}
401 filters[field] = {:operator => operator, :values => (values || [''])}
407 end
402 end
408 end
403 end
409
404
410 def add_short_filter(field, expression)
405 def add_short_filter(field, expression)
411 return unless expression && available_filters.has_key?(field)
406 return unless expression && available_filters.has_key?(field)
412 field_type = available_filters[field][:type]
407 field_type = available_filters[field][:type]
413 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
408 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
414 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
409 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
415 add_filter field, operator, $1.present? ? $1.split('|') : ['']
410 add_filter field, operator, $1.present? ? $1.split('|') : ['']
416 end || add_filter(field, '=', expression.split('|'))
411 end || add_filter(field, '=', expression.split('|'))
417 end
412 end
418
413
419 # Add multiple filters using +add_filter+
414 # Add multiple filters using +add_filter+
420 def add_filters(fields, operators, values)
415 def add_filters(fields, operators, values)
421 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
416 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
422 fields.each do |field|
417 fields.each do |field|
423 add_filter(field, operators[field], values && values[field])
418 add_filter(field, operators[field], values && values[field])
424 end
419 end
425 end
420 end
426 end
421 end
427
422
428 def has_filter?(field)
423 def has_filter?(field)
429 filters and filters[field]
424 filters and filters[field]
430 end
425 end
431
426
432 def type_for(field)
427 def type_for(field)
433 available_filters[field][:type] if available_filters.has_key?(field)
428 available_filters[field][:type] if available_filters.has_key?(field)
434 end
429 end
435
430
436 def operator_for(field)
431 def operator_for(field)
437 has_filter?(field) ? filters[field][:operator] : nil
432 has_filter?(field) ? filters[field][:operator] : nil
438 end
433 end
439
434
440 def values_for(field)
435 def values_for(field)
441 has_filter?(field) ? filters[field][:values] : nil
436 has_filter?(field) ? filters[field][:values] : nil
442 end
437 end
443
438
444 def value_for(field, index=0)
439 def value_for(field, index=0)
445 (values_for(field) || [])[index]
440 (values_for(field) || [])[index]
446 end
441 end
447
442
448 def label_for(field)
443 def label_for(field)
449 label = available_filters[field][:name] if available_filters.has_key?(field)
444 label = available_filters[field][:name] if available_filters.has_key?(field)
450 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
445 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
451 end
446 end
452
447
453 def available_columns
448 def available_columns
454 return @available_columns if @available_columns
449 return @available_columns if @available_columns
455 @available_columns = ::Query.available_columns.dup
450 @available_columns = ::Query.available_columns.dup
456 @available_columns += (project ?
451 @available_columns += (project ?
457 project.all_issue_custom_fields :
452 project.all_issue_custom_fields :
458 IssueCustomField.find(:all)
453 IssueCustomField.all
459 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
454 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
460
455
461 if User.current.allowed_to?(:view_time_entries, project, :global => true)
456 if User.current.allowed_to?(:view_time_entries, project, :global => true)
462 index = nil
457 index = nil
463 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
458 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
464 index = (index ? index + 1 : -1)
459 index = (index ? index + 1 : -1)
465 # insert the column after estimated_hours or at the end
460 # insert the column after estimated_hours or at the end
466 @available_columns.insert index, QueryColumn.new(:spent_hours,
461 @available_columns.insert index, QueryColumn.new(:spent_hours,
467 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
462 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
468 :default_order => 'desc',
463 :default_order => 'desc',
469 :caption => :label_spent_time
464 :caption => :label_spent_time
470 )
465 )
471 end
466 end
472
467
473 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
468 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
474 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
469 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
475 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
470 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
476 end
471 end
477
472
478 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
473 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
479 @available_columns.reject! {|column|
474 @available_columns.reject! {|column|
480 disabled_fields.include?(column.name.to_s)
475 disabled_fields.include?(column.name.to_s)
481 }
476 }
482
477
483 @available_columns
478 @available_columns
484 end
479 end
485
480
486 def self.available_columns=(v)
481 def self.available_columns=(v)
487 self.available_columns = (v)
482 self.available_columns = (v)
488 end
483 end
489
484
490 def self.add_available_column(column)
485 def self.add_available_column(column)
491 self.available_columns << (column) if column.is_a?(QueryColumn)
486 self.available_columns << (column) if column.is_a?(QueryColumn)
492 end
487 end
493
488
494 # Returns an array of columns that can be used to group the results
489 # Returns an array of columns that can be used to group the results
495 def groupable_columns
490 def groupable_columns
496 available_columns.select {|c| c.groupable}
491 available_columns.select {|c| c.groupable}
497 end
492 end
498
493
499 # Returns a Hash of columns and the key for sorting
494 # Returns a Hash of columns and the key for sorting
500 def sortable_columns
495 def sortable_columns
501 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
496 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
502 h[column.name.to_s] = column.sortable
497 h[column.name.to_s] = column.sortable
503 h
498 h
504 })
499 })
505 end
500 end
506
501
507 def columns
502 def columns
508 # preserve the column_names order
503 # preserve the column_names order
509 (has_default_columns? ? default_columns_names : column_names).collect do |name|
504 (has_default_columns? ? default_columns_names : column_names).collect do |name|
510 available_columns.find { |col| col.name == name }
505 available_columns.find { |col| col.name == name }
511 end.compact
506 end.compact
512 end
507 end
513
508
514 def default_columns_names
509 def default_columns_names
515 @default_columns_names ||= begin
510 @default_columns_names ||= begin
516 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
511 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
517
512
518 project.present? ? default_columns : [:project] | default_columns
513 project.present? ? default_columns : [:project] | default_columns
519 end
514 end
520 end
515 end
521
516
522 def column_names=(names)
517 def column_names=(names)
523 if names
518 if names
524 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
519 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
525 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
520 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
526 # Set column_names to nil if default columns
521 # Set column_names to nil if default columns
527 if names == default_columns_names
522 if names == default_columns_names
528 names = nil
523 names = nil
529 end
524 end
530 end
525 end
531 write_attribute(:column_names, names)
526 write_attribute(:column_names, names)
532 end
527 end
533
528
534 def has_column?(column)
529 def has_column?(column)
535 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
530 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
536 end
531 end
537
532
538 def has_default_columns?
533 def has_default_columns?
539 column_names.nil? || column_names.empty?
534 column_names.nil? || column_names.empty?
540 end
535 end
541
536
542 def sort_criteria=(arg)
537 def sort_criteria=(arg)
543 c = []
538 c = []
544 if arg.is_a?(Hash)
539 if arg.is_a?(Hash)
545 arg = arg.keys.sort.collect {|k| arg[k]}
540 arg = arg.keys.sort.collect {|k| arg[k]}
546 end
541 end
547 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
542 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
548 write_attribute(:sort_criteria, c)
543 write_attribute(:sort_criteria, c)
549 end
544 end
550
545
551 def sort_criteria
546 def sort_criteria
552 read_attribute(:sort_criteria) || []
547 read_attribute(:sort_criteria) || []
553 end
548 end
554
549
555 def sort_criteria_key(arg)
550 def sort_criteria_key(arg)
556 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
551 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
557 end
552 end
558
553
559 def sort_criteria_order(arg)
554 def sort_criteria_order(arg)
560 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
555 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
561 end
556 end
562
557
563 def sort_criteria_order_for(key)
558 def sort_criteria_order_for(key)
564 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
559 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
565 end
560 end
566
561
567 # Returns the SQL sort order that should be prepended for grouping
562 # Returns the SQL sort order that should be prepended for grouping
568 def group_by_sort_order
563 def group_by_sort_order
569 if grouped? && (column = group_by_column)
564 if grouped? && (column = group_by_column)
570 order = sort_criteria_order_for(column.name) || column.default_order
565 order = sort_criteria_order_for(column.name) || column.default_order
571 column.sortable.is_a?(Array) ?
566 column.sortable.is_a?(Array) ?
572 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
567 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
573 "#{column.sortable} #{order}"
568 "#{column.sortable} #{order}"
574 end
569 end
575 end
570 end
576
571
577 # Returns true if the query is a grouped query
572 # Returns true if the query is a grouped query
578 def grouped?
573 def grouped?
579 !group_by_column.nil?
574 !group_by_column.nil?
580 end
575 end
581
576
582 def group_by_column
577 def group_by_column
583 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
578 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
584 end
579 end
585
580
586 def group_by_statement
581 def group_by_statement
587 group_by_column.try(:groupable)
582 group_by_column.try(:groupable)
588 end
583 end
589
584
590 def project_statement
585 def project_statement
591 project_clauses = []
586 project_clauses = []
592 if project && !project.descendants.active.empty?
587 if project && !project.descendants.active.empty?
593 ids = [project.id]
588 ids = [project.id]
594 if has_filter?("subproject_id")
589 if has_filter?("subproject_id")
595 case operator_for("subproject_id")
590 case operator_for("subproject_id")
596 when '='
591 when '='
597 # include the selected subprojects
592 # include the selected subprojects
598 ids += values_for("subproject_id").each(&:to_i)
593 ids += values_for("subproject_id").each(&:to_i)
599 when '!*'
594 when '!*'
600 # main project only
595 # main project only
601 else
596 else
602 # all subprojects
597 # all subprojects
603 ids += project.descendants.collect(&:id)
598 ids += project.descendants.collect(&:id)
604 end
599 end
605 elsif Setting.display_subprojects_issues?
600 elsif Setting.display_subprojects_issues?
606 ids += project.descendants.collect(&:id)
601 ids += project.descendants.collect(&:id)
607 end
602 end
608 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
603 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
609 elsif project
604 elsif project
610 project_clauses << "#{Project.table_name}.id = %d" % project.id
605 project_clauses << "#{Project.table_name}.id = %d" % project.id
611 end
606 end
612 project_clauses.any? ? project_clauses.join(' AND ') : nil
607 project_clauses.any? ? project_clauses.join(' AND ') : nil
613 end
608 end
614
609
615 def statement
610 def statement
616 # filters clauses
611 # filters clauses
617 filters_clauses = []
612 filters_clauses = []
618 filters.each_key do |field|
613 filters.each_key do |field|
619 next if field == "subproject_id"
614 next if field == "subproject_id"
620 v = values_for(field).clone
615 v = values_for(field).clone
621 next unless v and !v.empty?
616 next unless v and !v.empty?
622 operator = operator_for(field)
617 operator = operator_for(field)
623
618
624 # "me" value subsitution
619 # "me" value subsitution
625 if %w(assigned_to_id author_id watcher_id).include?(field)
620 if %w(assigned_to_id author_id watcher_id).include?(field)
626 if v.delete("me")
621 if v.delete("me")
627 if User.current.logged?
622 if User.current.logged?
628 v.push(User.current.id.to_s)
623 v.push(User.current.id.to_s)
629 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
624 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
630 else
625 else
631 v.push("0")
626 v.push("0")
632 end
627 end
633 end
628 end
634 end
629 end
635
630
636 if field == 'project_id'
631 if field == 'project_id'
637 if v.delete('mine')
632 if v.delete('mine')
638 v += User.current.memberships.map(&:project_id).map(&:to_s)
633 v += User.current.memberships.map(&:project_id).map(&:to_s)
639 end
634 end
640 end
635 end
641
636
642 if field =~ /cf_(\d+)$/
637 if field =~ /cf_(\d+)$/
643 # custom field
638 # custom field
644 filters_clauses << sql_for_custom_field(field, operator, v, $1)
639 filters_clauses << sql_for_custom_field(field, operator, v, $1)
645 elsif respond_to?("sql_for_#{field}_field")
640 elsif respond_to?("sql_for_#{field}_field")
646 # specific statement
641 # specific statement
647 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
642 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
648 else
643 else
649 # regular field
644 # regular field
650 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
645 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
651 end
646 end
652 end if filters and valid?
647 end if filters and valid?
653
648
654 filters_clauses << project_statement
649 filters_clauses << project_statement
655 filters_clauses.reject!(&:blank?)
650 filters_clauses.reject!(&:blank?)
656
651
657 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
652 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
658 end
653 end
659
654
660 # Returns the issue count
655 # Returns the issue count
661 def issue_count
656 def issue_count
662 Issue.visible.count(:include => [:status, :project], :conditions => statement)
657 Issue.visible.count(:include => [:status, :project], :conditions => statement)
663 rescue ::ActiveRecord::StatementInvalid => e
658 rescue ::ActiveRecord::StatementInvalid => e
664 raise StatementInvalid.new(e.message)
659 raise StatementInvalid.new(e.message)
665 end
660 end
666
661
667 # Returns the issue count by group or nil if query is not grouped
662 # Returns the issue count by group or nil if query is not grouped
668 def issue_count_by_group
663 def issue_count_by_group
669 r = nil
664 r = nil
670 if grouped?
665 if grouped?
671 begin
666 begin
672 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
667 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
673 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
668 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
674 rescue ActiveRecord::RecordNotFound
669 rescue ActiveRecord::RecordNotFound
675 r = {nil => issue_count}
670 r = {nil => issue_count}
676 end
671 end
677 c = group_by_column
672 c = group_by_column
678 if c.is_a?(QueryCustomFieldColumn)
673 if c.is_a?(QueryCustomFieldColumn)
679 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
674 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
680 end
675 end
681 end
676 end
682 r
677 r
683 rescue ::ActiveRecord::StatementInvalid => e
678 rescue ::ActiveRecord::StatementInvalid => e
684 raise StatementInvalid.new(e.message)
679 raise StatementInvalid.new(e.message)
685 end
680 end
686
681
687 # Returns the issues
682 # Returns the issues
688 # Valid options are :order, :offset, :limit, :include, :conditions
683 # Valid options are :order, :offset, :limit, :include, :conditions
689 def issues(options={})
684 def issues(options={})
690 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
685 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
691 order_option = nil if order_option.blank?
686 order_option = nil if order_option.blank?
692
687
693 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
688 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
694 :conditions => statement,
689 :conditions => statement,
695 :order => order_option,
690 :order => order_option,
696 :joins => joins_for_order_statement(order_option),
691 :joins => joins_for_order_statement(order_option),
697 :limit => options[:limit],
692 :limit => options[:limit],
698 :offset => options[:offset]
693 :offset => options[:offset]
699
694
700 if has_column?(:spent_hours)
695 if has_column?(:spent_hours)
701 Issue.load_visible_spent_hours(issues)
696 Issue.load_visible_spent_hours(issues)
702 end
697 end
703 if has_column?(:relations)
698 if has_column?(:relations)
704 Issue.load_visible_relations(issues)
699 Issue.load_visible_relations(issues)
705 end
700 end
706 issues
701 issues
707 rescue ::ActiveRecord::StatementInvalid => e
702 rescue ::ActiveRecord::StatementInvalid => e
708 raise StatementInvalid.new(e.message)
703 raise StatementInvalid.new(e.message)
709 end
704 end
710
705
711 # Returns the issues ids
706 # Returns the issues ids
712 def issue_ids(options={})
707 def issue_ids(options={})
713 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
708 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
714 order_option = nil if order_option.blank?
709 order_option = nil if order_option.blank?
715
710
716 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
711 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
717 :conditions => statement,
712 :conditions => statement,
718 :order => order_option,
713 :order => order_option,
719 :joins => joins_for_order_statement(order_option),
714 :joins => joins_for_order_statement(order_option),
720 :limit => options[:limit],
715 :limit => options[:limit],
721 :offset => options[:offset]).find_ids
716 :offset => options[:offset]).find_ids
722 rescue ::ActiveRecord::StatementInvalid => e
717 rescue ::ActiveRecord::StatementInvalid => e
723 raise StatementInvalid.new(e.message)
718 raise StatementInvalid.new(e.message)
724 end
719 end
725
720
726 # Returns the journals
721 # Returns the journals
727 # Valid options are :order, :offset, :limit
722 # Valid options are :order, :offset, :limit
728 def journals(options={})
723 def journals(options={})
729 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
724 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
730 :conditions => statement,
725 :conditions => statement,
731 :order => options[:order],
726 :order => options[:order],
732 :limit => options[:limit],
727 :limit => options[:limit],
733 :offset => options[:offset]
728 :offset => options[:offset]
734 rescue ::ActiveRecord::StatementInvalid => e
729 rescue ::ActiveRecord::StatementInvalid => e
735 raise StatementInvalid.new(e.message)
730 raise StatementInvalid.new(e.message)
736 end
731 end
737
732
738 # Returns the versions
733 # Returns the versions
739 # Valid options are :conditions
734 # Valid options are :conditions
740 def versions(options={})
735 def versions(options={})
741 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
736 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
742 rescue ::ActiveRecord::StatementInvalid => e
737 rescue ::ActiveRecord::StatementInvalid => e
743 raise StatementInvalid.new(e.message)
738 raise StatementInvalid.new(e.message)
744 end
739 end
745
740
746 def sql_for_watcher_id_field(field, operator, value)
741 def sql_for_watcher_id_field(field, operator, value)
747 db_table = Watcher.table_name
742 db_table = Watcher.table_name
748 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
743 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
749 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
744 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
750 end
745 end
751
746
752 def sql_for_member_of_group_field(field, operator, value)
747 def sql_for_member_of_group_field(field, operator, value)
753 if operator == '*' # Any group
748 if operator == '*' # Any group
754 groups = Group.all
749 groups = Group.all
755 operator = '=' # Override the operator since we want to find by assigned_to
750 operator = '=' # Override the operator since we want to find by assigned_to
756 elsif operator == "!*"
751 elsif operator == "!*"
757 groups = Group.all
752 groups = Group.all
758 operator = '!' # Override the operator since we want to find by assigned_to
753 operator = '!' # Override the operator since we want to find by assigned_to
759 else
754 else
760 groups = Group.find_all_by_id(value)
755 groups = Group.find_all_by_id(value)
761 end
756 end
762 groups ||= []
757 groups ||= []
763
758
764 members_of_groups = groups.inject([]) {|user_ids, group|
759 members_of_groups = groups.inject([]) {|user_ids, group|
765 if group && group.user_ids.present?
760 if group && group.user_ids.present?
766 user_ids << group.user_ids
761 user_ids << group.user_ids
767 end
762 end
768 user_ids.flatten.uniq.compact
763 user_ids.flatten.uniq.compact
769 }.sort.collect(&:to_s)
764 }.sort.collect(&:to_s)
770
765
771 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
766 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
772 end
767 end
773
768
774 def sql_for_assigned_to_role_field(field, operator, value)
769 def sql_for_assigned_to_role_field(field, operator, value)
775 case operator
770 case operator
776 when "*", "!*" # Member / Not member
771 when "*", "!*" # Member / Not member
777 sw = operator == "!*" ? 'NOT' : ''
772 sw = operator == "!*" ? 'NOT' : ''
778 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
773 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
779 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
774 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
780 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
775 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
781 when "=", "!"
776 when "=", "!"
782 role_cond = value.any? ?
777 role_cond = value.any? ?
783 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
778 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
784 "1=0"
779 "1=0"
785
780
786 sw = operator == "!" ? 'NOT' : ''
781 sw = operator == "!" ? 'NOT' : ''
787 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
782 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
788 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
783 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
789 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
784 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
790 end
785 end
791 end
786 end
792
787
793 def sql_for_is_private_field(field, operator, value)
788 def sql_for_is_private_field(field, operator, value)
794 op = (operator == "=" ? 'IN' : 'NOT IN')
789 op = (operator == "=" ? 'IN' : 'NOT IN')
795 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
790 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
796
791
797 "#{Issue.table_name}.is_private #{op} (#{va})"
792 "#{Issue.table_name}.is_private #{op} (#{va})"
798 end
793 end
799
794
800 def sql_for_relations(field, operator, value, options={})
795 def sql_for_relations(field, operator, value, options={})
801 relation_options = IssueRelation::TYPES[field]
796 relation_options = IssueRelation::TYPES[field]
802 return relation_options unless relation_options
797 return relation_options unless relation_options
803
798
804 relation_type = field
799 relation_type = field
805 join_column, target_join_column = "issue_from_id", "issue_to_id"
800 join_column, target_join_column = "issue_from_id", "issue_to_id"
806 if relation_options[:reverse] || options[:reverse]
801 if relation_options[:reverse] || options[:reverse]
807 relation_type = relation_options[:reverse] || relation_type
802 relation_type = relation_options[:reverse] || relation_type
808 join_column, target_join_column = target_join_column, join_column
803 join_column, target_join_column = target_join_column, join_column
809 end
804 end
810
805
811 sql = case operator
806 sql = case operator
812 when "*", "!*"
807 when "*", "!*"
813 op = (operator == "*" ? 'IN' : 'NOT IN')
808 op = (operator == "*" ? 'IN' : 'NOT IN')
814 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
809 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
815 when "=", "!"
810 when "=", "!"
816 op = (operator == "=" ? 'IN' : 'NOT IN')
811 op = (operator == "=" ? 'IN' : 'NOT IN')
817 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
812 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
818 when "=p", "=!p", "!p"
813 when "=p", "=!p", "!p"
819 op = (operator == "!p" ? 'NOT IN' : 'IN')
814 op = (operator == "!p" ? 'NOT IN' : 'IN')
820 comp = (operator == "=!p" ? '<>' : '=')
815 comp = (operator == "=!p" ? '<>' : '=')
821 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
816 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
822 end
817 end
823
818
824 if relation_options[:sym] == field && !options[:reverse]
819 if relation_options[:sym] == field && !options[:reverse]
825 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
820 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
826 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
821 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
827 else
822 else
828 sql
823 sql
829 end
824 end
830 end
825 end
831
826
832 IssueRelation::TYPES.keys.each do |relation_type|
827 IssueRelation::TYPES.keys.each do |relation_type|
833 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
828 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
834 end
829 end
835
830
836 private
831 private
837
832
838 def sql_for_custom_field(field, operator, value, custom_field_id)
833 def sql_for_custom_field(field, operator, value, custom_field_id)
839 db_table = CustomValue.table_name
834 db_table = CustomValue.table_name
840 db_field = 'value'
835 db_field = 'value'
841 filter = @available_filters[field]
836 filter = @available_filters[field]
842 return nil unless filter
837 return nil unless filter
843 if filter[:format] == 'user'
838 if filter[:format] == 'user'
844 if value.delete('me')
839 if value.delete('me')
845 value.push User.current.id.to_s
840 value.push User.current.id.to_s
846 end
841 end
847 end
842 end
848 not_in = nil
843 not_in = nil
849 if operator == '!'
844 if operator == '!'
850 # Makes ! operator work for custom fields with multiple values
845 # Makes ! operator work for custom fields with multiple values
851 operator = '='
846 operator = '='
852 not_in = 'NOT'
847 not_in = 'NOT'
853 end
848 end
854 customized_key = "id"
849 customized_key = "id"
855 customized_class = Issue
850 customized_class = Issue
856 if field =~ /^(.+)\.cf_/
851 if field =~ /^(.+)\.cf_/
857 assoc = $1
852 assoc = $1
858 customized_key = "#{assoc}_id"
853 customized_key = "#{assoc}_id"
859 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
854 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
860 raise "Unknown Issue association #{assoc}" unless customized_class
855 raise "Unknown Issue association #{assoc}" unless customized_class
861 end
856 end
862 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
857 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
863 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
858 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
864 end
859 end
865
860
866 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
861 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
867 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
862 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
868 sql = ''
863 sql = ''
869 case operator
864 case operator
870 when "="
865 when "="
871 if value.any?
866 if value.any?
872 case type_for(field)
867 case type_for(field)
873 when :date, :date_past
868 when :date, :date_past
874 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
869 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
875 when :integer
870 when :integer
876 if is_custom_filter
871 if is_custom_filter
877 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
872 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
878 else
873 else
879 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
874 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
880 end
875 end
881 when :float
876 when :float
882 if is_custom_filter
877 if is_custom_filter
883 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
878 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
884 else
879 else
885 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
880 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
886 end
881 end
887 else
882 else
888 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
883 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
889 end
884 end
890 else
885 else
891 # IN an empty set
886 # IN an empty set
892 sql = "1=0"
887 sql = "1=0"
893 end
888 end
894 when "!"
889 when "!"
895 if value.any?
890 if value.any?
896 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
891 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
897 else
892 else
898 # NOT IN an empty set
893 # NOT IN an empty set
899 sql = "1=1"
894 sql = "1=1"
900 end
895 end
901 when "!*"
896 when "!*"
902 sql = "#{db_table}.#{db_field} IS NULL"
897 sql = "#{db_table}.#{db_field} IS NULL"
903 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
898 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
904 when "*"
899 when "*"
905 sql = "#{db_table}.#{db_field} IS NOT NULL"
900 sql = "#{db_table}.#{db_field} IS NOT NULL"
906 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
901 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
907 when ">="
902 when ">="
908 if [:date, :date_past].include?(type_for(field))
903 if [:date, :date_past].include?(type_for(field))
909 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
904 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
910 else
905 else
911 if is_custom_filter
906 if is_custom_filter
912 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
907 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
913 else
908 else
914 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
909 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
915 end
910 end
916 end
911 end
917 when "<="
912 when "<="
918 if [:date, :date_past].include?(type_for(field))
913 if [:date, :date_past].include?(type_for(field))
919 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
914 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
920 else
915 else
921 if is_custom_filter
916 if is_custom_filter
922 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
917 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
923 else
918 else
924 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
919 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
925 end
920 end
926 end
921 end
927 when "><"
922 when "><"
928 if [:date, :date_past].include?(type_for(field))
923 if [:date, :date_past].include?(type_for(field))
929 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
924 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
930 else
925 else
931 if is_custom_filter
926 if is_custom_filter
932 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
927 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
933 else
928 else
934 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
929 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
935 end
930 end
936 end
931 end
937 when "o"
932 when "o"
938 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
933 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
939 when "c"
934 when "c"
940 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
935 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
941 when "><t-"
936 when "><t-"
942 # between today - n days and today
937 # between today - n days and today
943 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
938 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
944 when ">t-"
939 when ">t-"
945 # >= today - n days
940 # >= today - n days
946 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
941 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
947 when "<t-"
942 when "<t-"
948 # <= today - n days
943 # <= today - n days
949 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
944 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
950 when "t-"
945 when "t-"
951 # = n days in past
946 # = n days in past
952 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
947 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
953 when "><t+"
948 when "><t+"
954 # between today and today + n days
949 # between today and today + n days
955 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
950 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
956 when ">t+"
951 when ">t+"
957 # >= today + n days
952 # >= today + n days
958 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
953 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
959 when "<t+"
954 when "<t+"
960 # <= today + n days
955 # <= today + n days
961 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
956 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
962 when "t+"
957 when "t+"
963 # = today + n days
958 # = today + n days
964 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
959 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
965 when "t"
960 when "t"
966 # = today
961 # = today
967 sql = relative_date_clause(db_table, db_field, 0, 0)
962 sql = relative_date_clause(db_table, db_field, 0, 0)
968 when "w"
963 when "w"
969 # = this week
964 # = this week
970 first_day_of_week = l(:general_first_day_of_week).to_i
965 first_day_of_week = l(:general_first_day_of_week).to_i
971 day_of_week = Date.today.cwday
966 day_of_week = Date.today.cwday
972 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
967 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
973 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
968 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
974 when "~"
969 when "~"
975 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
970 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
976 when "!~"
971 when "!~"
977 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
972 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
978 else
973 else
979 raise "Unknown query operator #{operator}"
974 raise "Unknown query operator #{operator}"
980 end
975 end
981
976
982 return sql
977 return sql
983 end
978 end
984
979
985 def add_custom_fields_filters(custom_fields, assoc=nil)
980 def add_custom_fields_filters(custom_fields, assoc=nil)
986 return unless custom_fields.present?
981 return unless custom_fields.present?
987 @available_filters ||= {}
982 @available_filters ||= {}
988
983
989 custom_fields.select(&:is_filter?).each do |field|
984 custom_fields.select(&:is_filter?).each do |field|
990 case field.field_format
985 case field.field_format
991 when "text"
986 when "text"
992 options = { :type => :text, :order => 20 }
987 options = { :type => :text, :order => 20 }
993 when "list"
988 when "list"
994 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
989 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
995 when "date"
990 when "date"
996 options = { :type => :date, :order => 20 }
991 options = { :type => :date, :order => 20 }
997 when "bool"
992 when "bool"
998 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
993 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
999 when "int"
994 when "int"
1000 options = { :type => :integer, :order => 20 }
995 options = { :type => :integer, :order => 20 }
1001 when "float"
996 when "float"
1002 options = { :type => :float, :order => 20 }
997 options = { :type => :float, :order => 20 }
1003 when "user", "version"
998 when "user", "version"
1004 next unless project
999 next unless project
1005 values = field.possible_values_options(project)
1000 values = field.possible_values_options(project)
1006 if User.current.logged? && field.field_format == 'user'
1001 if User.current.logged? && field.field_format == 'user'
1007 values.unshift ["<< #{l(:label_me)} >>", "me"]
1002 values.unshift ["<< #{l(:label_me)} >>", "me"]
1008 end
1003 end
1009 options = { :type => :list_optional, :values => values, :order => 20}
1004 options = { :type => :list_optional, :values => values, :order => 20}
1010 else
1005 else
1011 options = { :type => :string, :order => 20 }
1006 options = { :type => :string, :order => 20 }
1012 end
1007 end
1013 filter_id = "cf_#{field.id}"
1008 filter_id = "cf_#{field.id}"
1014 filter_name = field.name
1009 filter_name = field.name
1015 if assoc.present?
1010 if assoc.present?
1016 filter_id = "#{assoc}.#{filter_id}"
1011 filter_id = "#{assoc}.#{filter_id}"
1017 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1012 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1018 end
1013 end
1019 @available_filters[filter_id] = options.merge({
1014 @available_filters[filter_id] = options.merge({
1020 :name => filter_name,
1015 :name => filter_name,
1021 :format => field.field_format,
1016 :format => field.field_format,
1022 :field => field
1017 :field => field
1023 })
1018 })
1024 end
1019 end
1025 end
1020 end
1026
1021
1027 def add_associations_custom_fields_filters(*associations)
1022 def add_associations_custom_fields_filters(*associations)
1028 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1023 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1029 associations.each do |assoc|
1024 associations.each do |assoc|
1030 association_klass = Issue.reflect_on_association(assoc).klass
1025 association_klass = Issue.reflect_on_association(assoc).klass
1031 fields_by_class.each do |field_class, fields|
1026 fields_by_class.each do |field_class, fields|
1032 if field_class.customized_class <= association_klass
1027 if field_class.customized_class <= association_klass
1033 add_custom_fields_filters(fields, assoc)
1028 add_custom_fields_filters(fields, assoc)
1034 end
1029 end
1035 end
1030 end
1036 end
1031 end
1037 end
1032 end
1038
1033
1039 # Returns a SQL clause for a date or datetime field.
1034 # Returns a SQL clause for a date or datetime field.
1040 def date_clause(table, field, from, to)
1035 def date_clause(table, field, from, to)
1041 s = []
1036 s = []
1042 if from
1037 if from
1043 from_yesterday = from - 1
1038 from_yesterday = from - 1
1044 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1039 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1045 if self.class.default_timezone == :utc
1040 if self.class.default_timezone == :utc
1046 from_yesterday_time = from_yesterday_time.utc
1041 from_yesterday_time = from_yesterday_time.utc
1047 end
1042 end
1048 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1043 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1049 end
1044 end
1050 if to
1045 if to
1051 to_time = Time.local(to.year, to.month, to.day)
1046 to_time = Time.local(to.year, to.month, to.day)
1052 if self.class.default_timezone == :utc
1047 if self.class.default_timezone == :utc
1053 to_time = to_time.utc
1048 to_time = to_time.utc
1054 end
1049 end
1055 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1050 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1056 end
1051 end
1057 s.join(' AND ')
1052 s.join(' AND ')
1058 end
1053 end
1059
1054
1060 # Returns a SQL clause for a date or datetime field using relative dates.
1055 # Returns a SQL clause for a date or datetime field using relative dates.
1061 def relative_date_clause(table, field, days_from, days_to)
1056 def relative_date_clause(table, field, days_from, days_to)
1062 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1057 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1063 end
1058 end
1064
1059
1065 # Additional joins required for the given sort options
1060 # Additional joins required for the given sort options
1066 def joins_for_order_statement(order_options)
1061 def joins_for_order_statement(order_options)
1067 joins = []
1062 joins = []
1068
1063
1069 if order_options
1064 if order_options
1070 if order_options.include?('authors')
1065 if order_options.include?('authors')
1071 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1066 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1072 end
1067 end
1073 order_options.scan(/cf_\d+/).uniq.each do |name|
1068 order_options.scan(/cf_\d+/).uniq.each do |name|
1074 column = available_columns.detect {|c| c.name.to_s == name}
1069 column = available_columns.detect {|c| c.name.to_s == name}
1075 join = column && column.custom_field.join_for_order_statement
1070 join = column && column.custom_field.join_for_order_statement
1076 if join
1071 if join
1077 joins << join
1072 joins << join
1078 end
1073 end
1079 end
1074 end
1080 end
1075 end
1081
1076
1082 joins.any? ? joins.join(' ') : nil
1077 joins.any? ? joins.join(' ') : nil
1083 end
1078 end
1084 end
1079 end
@@ -1,435 +1,435
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 ScmFetchError < Exception; end
18 class ScmFetchError < Exception; end
19
19
20 class Repository < ActiveRecord::Base
20 class Repository < ActiveRecord::Base
21 include Redmine::Ciphering
21 include Redmine::Ciphering
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23
23
24 # Maximum length for repository identifiers
24 # Maximum length for repository identifiers
25 IDENTIFIER_MAX_LENGTH = 255
25 IDENTIFIER_MAX_LENGTH = 255
26
26
27 belongs_to :project
27 belongs_to :project
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
28 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
29 has_many :filechanges, :class_name => 'Change', :through => :changesets
30
30
31 serialize :extra_info
31 serialize :extra_info
32
32
33 before_save :check_default
33 before_save :check_default
34
34
35 # Raw SQL to delete changesets and changes in the database
35 # Raw SQL to delete changesets and changes in the database
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
36 # has_many :changesets, :dependent => :destroy is too slow for big repositories
37 before_destroy :clear_changesets
37 before_destroy :clear_changesets
38
38
39 validates_length_of :password, :maximum => 255, :allow_nil => true
39 validates_length_of :password, :maximum => 255, :allow_nil => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
40 validates_length_of :identifier, :maximum => IDENTIFIER_MAX_LENGTH, :allow_blank => true
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
41 validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
42 validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
43 validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
44 # donwcase letters, digits, dashes, underscores but not digits only
44 # donwcase letters, digits, dashes, underscores but not digits only
45 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true
45 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :allow_blank => true
46 # Checks if the SCM is enabled when creating a repository
46 # Checks if the SCM is enabled when creating a repository
47 validate :repo_create_validation, :on => :create
47 validate :repo_create_validation, :on => :create
48
48
49 safe_attributes 'identifier',
49 safe_attributes 'identifier',
50 'login',
50 'login',
51 'password',
51 'password',
52 'path_encoding',
52 'path_encoding',
53 'log_encoding',
53 'log_encoding',
54 'is_default'
54 'is_default'
55
55
56 safe_attributes 'url',
56 safe_attributes 'url',
57 :if => lambda {|repository, user| repository.new_record?}
57 :if => lambda {|repository, user| repository.new_record?}
58
58
59 def repo_create_validation
59 def repo_create_validation
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
60 unless Setting.enabled_scm.include?(self.class.name.demodulize)
61 errors.add(:type, :invalid)
61 errors.add(:type, :invalid)
62 end
62 end
63 end
63 end
64
64
65 def self.human_attribute_name(attribute_key_name, *args)
65 def self.human_attribute_name(attribute_key_name, *args)
66 attr_name = attribute_key_name.to_s
66 attr_name = attribute_key_name.to_s
67 if attr_name == "log_encoding"
67 if attr_name == "log_encoding"
68 attr_name = "commit_logs_encoding"
68 attr_name = "commit_logs_encoding"
69 end
69 end
70 super(attr_name, *args)
70 super(attr_name, *args)
71 end
71 end
72
72
73 # Removes leading and trailing whitespace
73 # Removes leading and trailing whitespace
74 def url=(arg)
74 def url=(arg)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
75 write_attribute(:url, arg ? arg.to_s.strip : nil)
76 end
76 end
77
77
78 # Removes leading and trailing whitespace
78 # Removes leading and trailing whitespace
79 def root_url=(arg)
79 def root_url=(arg)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
80 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
81 end
81 end
82
82
83 def password
83 def password
84 read_ciphered_attribute(:password)
84 read_ciphered_attribute(:password)
85 end
85 end
86
86
87 def password=(arg)
87 def password=(arg)
88 write_ciphered_attribute(:password, arg)
88 write_ciphered_attribute(:password, arg)
89 end
89 end
90
90
91 def scm_adapter
91 def scm_adapter
92 self.class.scm_adapter_class
92 self.class.scm_adapter_class
93 end
93 end
94
94
95 def scm
95 def scm
96 unless @scm
96 unless @scm
97 @scm = self.scm_adapter.new(url, root_url,
97 @scm = self.scm_adapter.new(url, root_url,
98 login, password, path_encoding)
98 login, password, path_encoding)
99 if root_url.blank? && @scm.root_url.present?
99 if root_url.blank? && @scm.root_url.present?
100 update_attribute(:root_url, @scm.root_url)
100 update_attribute(:root_url, @scm.root_url)
101 end
101 end
102 end
102 end
103 @scm
103 @scm
104 end
104 end
105
105
106 def scm_name
106 def scm_name
107 self.class.scm_name
107 self.class.scm_name
108 end
108 end
109
109
110 def name
110 def name
111 if identifier.present?
111 if identifier.present?
112 identifier
112 identifier
113 elsif is_default?
113 elsif is_default?
114 l(:field_repository_is_default)
114 l(:field_repository_is_default)
115 else
115 else
116 scm_name
116 scm_name
117 end
117 end
118 end
118 end
119
119
120 def identifier=(identifier)
120 def identifier=(identifier)
121 super unless identifier_frozen?
121 super unless identifier_frozen?
122 end
122 end
123
123
124 def identifier_frozen?
124 def identifier_frozen?
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
125 errors[:identifier].blank? && !(new_record? || identifier.blank?)
126 end
126 end
127
127
128 def identifier_param
128 def identifier_param
129 if is_default?
129 if is_default?
130 nil
130 nil
131 elsif identifier.present?
131 elsif identifier.present?
132 identifier
132 identifier
133 else
133 else
134 id.to_s
134 id.to_s
135 end
135 end
136 end
136 end
137
137
138 def <=>(repository)
138 def <=>(repository)
139 if is_default?
139 if is_default?
140 -1
140 -1
141 elsif repository.is_default?
141 elsif repository.is_default?
142 1
142 1
143 else
143 else
144 identifier.to_s <=> repository.identifier.to_s
144 identifier.to_s <=> repository.identifier.to_s
145 end
145 end
146 end
146 end
147
147
148 def self.find_by_identifier_param(param)
148 def self.find_by_identifier_param(param)
149 if param.to_s =~ /^\d+$/
149 if param.to_s =~ /^\d+$/
150 find_by_id(param)
150 find_by_id(param)
151 else
151 else
152 find_by_identifier(param)
152 find_by_identifier(param)
153 end
153 end
154 end
154 end
155
155
156 def merge_extra_info(arg)
156 def merge_extra_info(arg)
157 h = extra_info || {}
157 h = extra_info || {}
158 return h if arg.nil?
158 return h if arg.nil?
159 h.merge!(arg)
159 h.merge!(arg)
160 write_attribute(:extra_info, h)
160 write_attribute(:extra_info, h)
161 end
161 end
162
162
163 def report_last_commit
163 def report_last_commit
164 true
164 true
165 end
165 end
166
166
167 def supports_cat?
167 def supports_cat?
168 scm.supports_cat?
168 scm.supports_cat?
169 end
169 end
170
170
171 def supports_annotate?
171 def supports_annotate?
172 scm.supports_annotate?
172 scm.supports_annotate?
173 end
173 end
174
174
175 def supports_all_revisions?
175 def supports_all_revisions?
176 true
176 true
177 end
177 end
178
178
179 def supports_directory_revisions?
179 def supports_directory_revisions?
180 false
180 false
181 end
181 end
182
182
183 def supports_revision_graph?
183 def supports_revision_graph?
184 false
184 false
185 end
185 end
186
186
187 def entry(path=nil, identifier=nil)
187 def entry(path=nil, identifier=nil)
188 scm.entry(path, identifier)
188 scm.entry(path, identifier)
189 end
189 end
190
190
191 def entries(path=nil, identifier=nil)
191 def entries(path=nil, identifier=nil)
192 entries = scm.entries(path, identifier)
192 entries = scm.entries(path, identifier)
193 load_entries_changesets(entries)
193 load_entries_changesets(entries)
194 entries
194 entries
195 end
195 end
196
196
197 def branches
197 def branches
198 scm.branches
198 scm.branches
199 end
199 end
200
200
201 def tags
201 def tags
202 scm.tags
202 scm.tags
203 end
203 end
204
204
205 def default_branch
205 def default_branch
206 nil
206 nil
207 end
207 end
208
208
209 def properties(path, identifier=nil)
209 def properties(path, identifier=nil)
210 scm.properties(path, identifier)
210 scm.properties(path, identifier)
211 end
211 end
212
212
213 def cat(path, identifier=nil)
213 def cat(path, identifier=nil)
214 scm.cat(path, identifier)
214 scm.cat(path, identifier)
215 end
215 end
216
216
217 def diff(path, rev, rev_to)
217 def diff(path, rev, rev_to)
218 scm.diff(path, rev, rev_to)
218 scm.diff(path, rev, rev_to)
219 end
219 end
220
220
221 def diff_format_revisions(cs, cs_to, sep=':')
221 def diff_format_revisions(cs, cs_to, sep=':')
222 text = ""
222 text = ""
223 text << cs_to.format_identifier + sep if cs_to
223 text << cs_to.format_identifier + sep if cs_to
224 text << cs.format_identifier if cs
224 text << cs.format_identifier if cs
225 text
225 text
226 end
226 end
227
227
228 # Returns a path relative to the url of the repository
228 # Returns a path relative to the url of the repository
229 def relative_path(path)
229 def relative_path(path)
230 path
230 path
231 end
231 end
232
232
233 # Finds and returns a revision with a number or the beginning of a hash
233 # Finds and returns a revision with a number or the beginning of a hash
234 def find_changeset_by_name(name)
234 def find_changeset_by_name(name)
235 return nil if name.blank?
235 return nil if name.blank?
236 s = name.to_s
236 s = name.to_s
237 changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
237 changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
238 ["revision = ?", s] : ["revision LIKE ?", s + '%']))
238 ["revision = ?", s] : ["revision LIKE ?", s + '%']))
239 end
239 end
240
240
241 def latest_changeset
241 def latest_changeset
242 @latest_changeset ||= changesets.find(:first)
242 @latest_changeset ||= changesets.find(:first)
243 end
243 end
244
244
245 # Returns the latest changesets for +path+
245 # Returns the latest changesets for +path+
246 # Default behaviour is to search in cached changesets
246 # Default behaviour is to search in cached changesets
247 def latest_changesets(path, rev, limit=10)
247 def latest_changesets(path, rev, limit=10)
248 if path.blank?
248 if path.blank?
249 changesets.find(
249 changesets.find(
250 :all,
250 :all,
251 :include => :user,
251 :include => :user,
252 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
252 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
253 :limit => limit)
253 :limit => limit)
254 else
254 else
255 filechanges.find(
255 filechanges.find(
256 :all,
256 :all,
257 :include => {:changeset => :user},
257 :include => {:changeset => :user},
258 :conditions => ["path = ?", path.with_leading_slash],
258 :conditions => ["path = ?", path.with_leading_slash],
259 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
259 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
260 :limit => limit
260 :limit => limit
261 ).collect(&:changeset)
261 ).collect(&:changeset)
262 end
262 end
263 end
263 end
264
264
265 def scan_changesets_for_issue_ids
265 def scan_changesets_for_issue_ids
266 self.changesets.each(&:scan_comment_for_issue_ids)
266 self.changesets.each(&:scan_comment_for_issue_ids)
267 end
267 end
268
268
269 # Returns an array of committers usernames and associated user_id
269 # Returns an array of committers usernames and associated user_id
270 def committers
270 def committers
271 @committers ||= Changeset.connection.select_rows(
271 @committers ||= Changeset.connection.select_rows(
272 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
272 "SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
273 end
273 end
274
274
275 # Maps committers username to a user ids
275 # Maps committers username to a user ids
276 def committer_ids=(h)
276 def committer_ids=(h)
277 if h.is_a?(Hash)
277 if h.is_a?(Hash)
278 committers.each do |committer, user_id|
278 committers.each do |committer, user_id|
279 new_user_id = h[committer]
279 new_user_id = h[committer]
280 if new_user_id && (new_user_id.to_i != user_id.to_i)
280 if new_user_id && (new_user_id.to_i != user_id.to_i)
281 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
281 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
282 Changeset.update_all(
282 Changeset.update_all(
283 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
283 "user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
284 ["repository_id = ? AND committer = ?", id, committer])
284 ["repository_id = ? AND committer = ?", id, committer])
285 end
285 end
286 end
286 end
287 @committers = nil
287 @committers = nil
288 @found_committer_users = nil
288 @found_committer_users = nil
289 true
289 true
290 else
290 else
291 false
291 false
292 end
292 end
293 end
293 end
294
294
295 # Returns the Redmine User corresponding to the given +committer+
295 # Returns the Redmine User corresponding to the given +committer+
296 # It will return nil if the committer is not yet mapped and if no User
296 # It will return nil if the committer is not yet mapped and if no User
297 # with the same username or email was found
297 # with the same username or email was found
298 def find_committer_user(committer)
298 def find_committer_user(committer)
299 unless committer.blank?
299 unless committer.blank?
300 @found_committer_users ||= {}
300 @found_committer_users ||= {}
301 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
301 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
302
302
303 user = nil
303 user = nil
304 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
304 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
305 if c && c.user
305 if c && c.user
306 user = c.user
306 user = c.user
307 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
307 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
308 username, email = $1.strip, $3
308 username, email = $1.strip, $3
309 u = User.find_by_login(username)
309 u = User.find_by_login(username)
310 u ||= User.find_by_mail(email) unless email.blank?
310 u ||= User.find_by_mail(email) unless email.blank?
311 user = u
311 user = u
312 end
312 end
313 @found_committer_users[committer] = user
313 @found_committer_users[committer] = user
314 user
314 user
315 end
315 end
316 end
316 end
317
317
318 def repo_log_encoding
318 def repo_log_encoding
319 encoding = log_encoding.to_s.strip
319 encoding = log_encoding.to_s.strip
320 encoding.blank? ? 'UTF-8' : encoding
320 encoding.blank? ? 'UTF-8' : encoding
321 end
321 end
322
322
323 # Fetches new changesets for all repositories of active projects
323 # Fetches new changesets for all repositories of active projects
324 # Can be called periodically by an external script
324 # Can be called periodically by an external script
325 # eg. ruby script/runner "Repository.fetch_changesets"
325 # eg. ruby script/runner "Repository.fetch_changesets"
326 def self.fetch_changesets
326 def self.fetch_changesets
327 Project.active.has_module(:repository).all.each do |project|
327 Project.active.has_module(:repository).all.each do |project|
328 project.repositories.each do |repository|
328 project.repositories.each do |repository|
329 begin
329 begin
330 repository.fetch_changesets
330 repository.fetch_changesets
331 rescue Redmine::Scm::Adapters::CommandFailed => e
331 rescue Redmine::Scm::Adapters::CommandFailed => e
332 logger.error "scm: error during fetching changesets: #{e.message}"
332 logger.error "scm: error during fetching changesets: #{e.message}"
333 end
333 end
334 end
334 end
335 end
335 end
336 end
336 end
337
337
338 # scan changeset comments to find related and fixed issues for all repositories
338 # scan changeset comments to find related and fixed issues for all repositories
339 def self.scan_changesets_for_issue_ids
339 def self.scan_changesets_for_issue_ids
340 find(:all).each(&:scan_changesets_for_issue_ids)
340 all.each(&:scan_changesets_for_issue_ids)
341 end
341 end
342
342
343 def self.scm_name
343 def self.scm_name
344 'Abstract'
344 'Abstract'
345 end
345 end
346
346
347 def self.available_scm
347 def self.available_scm
348 subclasses.collect {|klass| [klass.scm_name, klass.name]}
348 subclasses.collect {|klass| [klass.scm_name, klass.name]}
349 end
349 end
350
350
351 def self.factory(klass_name, *args)
351 def self.factory(klass_name, *args)
352 klass = "Repository::#{klass_name}".constantize
352 klass = "Repository::#{klass_name}".constantize
353 klass.new(*args)
353 klass.new(*args)
354 rescue
354 rescue
355 nil
355 nil
356 end
356 end
357
357
358 def self.scm_adapter_class
358 def self.scm_adapter_class
359 nil
359 nil
360 end
360 end
361
361
362 def self.scm_command
362 def self.scm_command
363 ret = ""
363 ret = ""
364 begin
364 begin
365 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
365 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
366 rescue Exception => e
366 rescue Exception => e
367 logger.error "scm: error during get command: #{e.message}"
367 logger.error "scm: error during get command: #{e.message}"
368 end
368 end
369 ret
369 ret
370 end
370 end
371
371
372 def self.scm_version_string
372 def self.scm_version_string
373 ret = ""
373 ret = ""
374 begin
374 begin
375 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
375 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
376 rescue Exception => e
376 rescue Exception => e
377 logger.error "scm: error during get version string: #{e.message}"
377 logger.error "scm: error during get version string: #{e.message}"
378 end
378 end
379 ret
379 ret
380 end
380 end
381
381
382 def self.scm_available
382 def self.scm_available
383 ret = false
383 ret = false
384 begin
384 begin
385 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
385 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
386 rescue Exception => e
386 rescue Exception => e
387 logger.error "scm: error during get scm available: #{e.message}"
387 logger.error "scm: error during get scm available: #{e.message}"
388 end
388 end
389 ret
389 ret
390 end
390 end
391
391
392 def set_as_default?
392 def set_as_default?
393 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
393 new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
394 end
394 end
395
395
396 protected
396 protected
397
397
398 def check_default
398 def check_default
399 if !is_default? && set_as_default?
399 if !is_default? && set_as_default?
400 self.is_default = true
400 self.is_default = true
401 end
401 end
402 if is_default? && is_default_changed?
402 if is_default? && is_default_changed?
403 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
403 Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
404 end
404 end
405 end
405 end
406
406
407 def load_entries_changesets(entries)
407 def load_entries_changesets(entries)
408 if entries
408 if entries
409 entries.each do |entry|
409 entries.each do |entry|
410 if entry.lastrev && entry.lastrev.identifier
410 if entry.lastrev && entry.lastrev.identifier
411 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
411 entry.changeset = find_changeset_by_name(entry.lastrev.identifier)
412 end
412 end
413 end
413 end
414 end
414 end
415 end
415 end
416
416
417 private
417 private
418
418
419 # Deletes repository data
419 # Deletes repository data
420 def clear_changesets
420 def clear_changesets
421 cs = Changeset.table_name
421 cs = Changeset.table_name
422 ch = Change.table_name
422 ch = Change.table_name
423 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
423 ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
424 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
424 cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
425
425
426 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
426 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
427 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
427 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
428 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
428 connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
429 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
429 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
430 clear_extra_info_of_changesets
430 clear_extra_info_of_changesets
431 end
431 end
432
432
433 def clear_extra_info_of_changesets
433 def clear_extra_info_of_changesets
434 end
434 end
435 end
435 end
@@ -1,158 +1,159
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 'redmine/scm/adapters/mercurial_adapter'
18 require 'redmine/scm/adapters/mercurial_adapter'
19
19
20 class Repository::Mercurial < Repository
20 class Repository::Mercurial < Repository
21 # sort changesets by revision number
21 # sort changesets by revision number
22 has_many :changesets,
22 has_many :changesets,
23 :order => "#{Changeset.table_name}.id DESC",
23 :order => "#{Changeset.table_name}.id DESC",
24 :foreign_key => 'repository_id'
24 :foreign_key => 'repository_id'
25
25
26 attr_protected :root_url
26 attr_protected :root_url
27 validates_presence_of :url
27 validates_presence_of :url
28
28
29 # number of changesets to fetch at once
29 # number of changesets to fetch at once
30 FETCH_AT_ONCE = 100
30 FETCH_AT_ONCE = 100
31
31
32 def self.human_attribute_name(attribute_key_name, *args)
32 def self.human_attribute_name(attribute_key_name, *args)
33 attr_name = attribute_key_name.to_s
33 attr_name = attribute_key_name.to_s
34 if attr_name == "url"
34 if attr_name == "url"
35 attr_name = "path_to_repository"
35 attr_name = "path_to_repository"
36 end
36 end
37 super(attr_name, *args)
37 super(attr_name, *args)
38 end
38 end
39
39
40 def self.scm_adapter_class
40 def self.scm_adapter_class
41 Redmine::Scm::Adapters::MercurialAdapter
41 Redmine::Scm::Adapters::MercurialAdapter
42 end
42 end
43
43
44 def self.scm_name
44 def self.scm_name
45 'Mercurial'
45 'Mercurial'
46 end
46 end
47
47
48 def supports_directory_revisions?
48 def supports_directory_revisions?
49 true
49 true
50 end
50 end
51
51
52 def supports_revision_graph?
52 def supports_revision_graph?
53 true
53 true
54 end
54 end
55
55
56 def repo_log_encoding
56 def repo_log_encoding
57 'UTF-8'
57 'UTF-8'
58 end
58 end
59
59
60 # Returns the readable identifier for the given mercurial changeset
60 # Returns the readable identifier for the given mercurial changeset
61 def self.format_changeset_identifier(changeset)
61 def self.format_changeset_identifier(changeset)
62 "#{changeset.revision}:#{changeset.scmid}"
62 "#{changeset.revision}:#{changeset.scmid}"
63 end
63 end
64
64
65 # Returns the identifier for the given Mercurial changeset
65 # Returns the identifier for the given Mercurial changeset
66 def self.changeset_identifier(changeset)
66 def self.changeset_identifier(changeset)
67 changeset.scmid
67 changeset.scmid
68 end
68 end
69
69
70 def diff_format_revisions(cs, cs_to, sep=':')
70 def diff_format_revisions(cs, cs_to, sep=':')
71 super(cs, cs_to, ' ')
71 super(cs, cs_to, ' ')
72 end
72 end
73
73
74 # Finds and returns a revision with a number or the beginning of a hash
74 # Finds and returns a revision with a number or the beginning of a hash
75 def find_changeset_by_name(name)
75 def find_changeset_by_name(name)
76 return nil if name.blank?
76 return nil if name.blank?
77 s = name.to_s
77 s = name.to_s
78 if /[^\d]/ =~ s or s.size > 8
78 if /[^\d]/ =~ s or s.size > 8
79 cs = changesets.where(:scmid => s).first
79 cs = changesets.where(:scmid => s).first
80 else
80 else
81 cs = changesets.where(:revision => s).first
81 cs = changesets.where(:revision => s).first
82 end
82 end
83 return cs if cs
83 return cs if cs
84 changesets.where('scmid LIKE ?', "#{s}%").first
84 changesets.where('scmid LIKE ?', "#{s}%").first
85 end
85 end
86
86
87 # Returns the latest changesets for +path+; sorted by revision number
87 # Returns the latest changesets for +path+; sorted by revision number
88 #
88 #
89 # Because :order => 'id DESC' is defined at 'has_many',
89 # Because :order => 'id DESC' is defined at 'has_many',
90 # there is no need to set 'order'.
90 # there is no need to set 'order'.
91 # But, MySQL test fails.
91 # But, MySQL test fails.
92 # Sqlite3 and PostgreSQL pass.
92 # Sqlite3 and PostgreSQL pass.
93 # Is this MySQL bug?
93 # Is this MySQL bug?
94 def latest_changesets(path, rev, limit=10)
94 def latest_changesets(path, rev, limit=10)
95 changesets.find(:all,
95 changesets.
96 :include => :user,
96 includes(:user).
97 :conditions => latest_changesets_cond(path, rev, limit),
97 where(latest_changesets_cond(path, rev, limit)).
98 :limit => limit,
98 limit(limit).
99 :order => "#{Changeset.table_name}.id DESC")
99 order("#{Changeset.table_name}.id DESC").
100 all
100 end
101 end
101
102
102 def latest_changesets_cond(path, rev, limit)
103 def latest_changesets_cond(path, rev, limit)
103 cond, args = [], []
104 cond, args = [], []
104 if scm.branchmap.member? rev
105 if scm.branchmap.member? rev
105 # Mercurial named branch is *stable* in each revision.
106 # Mercurial named branch is *stable* in each revision.
106 # So, named branch can be stored in database.
107 # So, named branch can be stored in database.
107 # Mercurial provides *bookmark* which is equivalent with git branch.
108 # Mercurial provides *bookmark* which is equivalent with git branch.
108 # But, bookmark is not implemented.
109 # But, bookmark is not implemented.
109 cond << "#{Changeset.table_name}.scmid IN (?)"
110 cond << "#{Changeset.table_name}.scmid IN (?)"
110 # Revisions in root directory and sub directory are not equal.
111 # Revisions in root directory and sub directory are not equal.
111 # So, in order to get correct limit, we need to get all revisions.
112 # So, in order to get correct limit, we need to get all revisions.
112 # But, it is very heavy.
113 # But, it is very heavy.
113 # Mercurial does not treat direcotry.
114 # Mercurial does not treat direcotry.
114 # So, "hg log DIR" is very heavy.
115 # So, "hg log DIR" is very heavy.
115 branch_limit = path.blank? ? limit : ( limit * 5 )
116 branch_limit = path.blank? ? limit : ( limit * 5 )
116 args << scm.nodes_in_branch(rev, :limit => branch_limit)
117 args << scm.nodes_in_branch(rev, :limit => branch_limit)
117 elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
118 elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
118 cond << "#{Changeset.table_name}.id <= ?"
119 cond << "#{Changeset.table_name}.id <= ?"
119 args << last.id
120 args << last.id
120 end
121 end
121 unless path.blank?
122 unless path.blank?
122 cond << "EXISTS (SELECT * FROM #{Change.table_name}
123 cond << "EXISTS (SELECT * FROM #{Change.table_name}
123 WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
124 WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
124 AND (#{Change.table_name}.path = ?
125 AND (#{Change.table_name}.path = ?
125 OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
126 OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
126 args << path.with_leading_slash
127 args << path.with_leading_slash
127 args << "#{path.with_leading_slash.gsub(%r{[%_\\]}) { |s| "\\#{s}" }}/%" << '\\'
128 args << "#{path.with_leading_slash.gsub(%r{[%_\\]}) { |s| "\\#{s}" }}/%" << '\\'
128 end
129 end
129 [cond.join(' AND '), *args] unless cond.empty?
130 [cond.join(' AND '), *args] unless cond.empty?
130 end
131 end
131 private :latest_changesets_cond
132 private :latest_changesets_cond
132
133
133 def fetch_changesets
134 def fetch_changesets
134 return if scm.info.nil?
135 return if scm.info.nil?
135 scm_rev = scm.info.lastrev.revision.to_i
136 scm_rev = scm.info.lastrev.revision.to_i
136 db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
137 db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
137 return unless db_rev < scm_rev # already up-to-date
138 return unless db_rev < scm_rev # already up-to-date
138
139
139 logger.debug "Fetching changesets for repository #{url}" if logger
140 logger.debug "Fetching changesets for repository #{url}" if logger
140 (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
141 (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
141 scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
142 scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
142 transaction do
143 transaction do
143 parents = (re.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
144 parents = (re.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
144 cs = Changeset.create(:repository => self,
145 cs = Changeset.create(:repository => self,
145 :revision => re.revision,
146 :revision => re.revision,
146 :scmid => re.scmid,
147 :scmid => re.scmid,
147 :committer => re.author,
148 :committer => re.author,
148 :committed_on => re.time,
149 :committed_on => re.time,
149 :comments => re.message,
150 :comments => re.message,
150 :parents => parents)
151 :parents => parents)
151 unless cs.new_record?
152 unless cs.new_record?
152 re.paths.each { |e| cs.create_change(e) }
153 re.paths.each { |e| cs.create_change(e) }
153 end
154 end
154 end
155 end
155 end
156 end
156 end
157 end
157 end
158 end
158 end
159 end
@@ -1,66 +1,66
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Watcher < ActiveRecord::Base
18 class Watcher < ActiveRecord::Base
19 belongs_to :watchable, :polymorphic => true
19 belongs_to :watchable, :polymorphic => true
20 belongs_to :user
20 belongs_to :user
21
21
22 validates_presence_of :user
22 validates_presence_of :user
23 validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
23 validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
24 validate :validate_user
24 validate :validate_user
25
25
26 # Unwatch things that users are no longer allowed to view
26 # Unwatch things that users are no longer allowed to view
27 def self.prune(options={})
27 def self.prune(options={})
28 if options.has_key?(:user)
28 if options.has_key?(:user)
29 prune_single_user(options[:user], options)
29 prune_single_user(options[:user], options)
30 else
30 else
31 pruned = 0
31 pruned = 0
32 User.find(:all, :conditions => "id IN (SELECT DISTINCT user_id FROM #{table_name})").each do |user|
32 User.where("id IN (SELECT DISTINCT user_id FROM #{table_name})").all.each do |user|
33 pruned += prune_single_user(user, options)
33 pruned += prune_single_user(user, options)
34 end
34 end
35 pruned
35 pruned
36 end
36 end
37 end
37 end
38
38
39 protected
39 protected
40
40
41 def validate_user
41 def validate_user
42 errors.add :user_id, :invalid unless user.nil? || user.active?
42 errors.add :user_id, :invalid unless user.nil? || user.active?
43 end
43 end
44
44
45 private
45 private
46
46
47 def self.prune_single_user(user, options={})
47 def self.prune_single_user(user, options={})
48 return unless user.is_a?(User)
48 return unless user.is_a?(User)
49 pruned = 0
49 pruned = 0
50 find(:all, :conditions => {:user_id => user.id}).each do |watcher|
50 where(:user_id => user.id).all.each do |watcher|
51 next if watcher.watchable.nil?
51 next if watcher.watchable.nil?
52
52
53 if options.has_key?(:project)
53 if options.has_key?(:project)
54 next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project]
54 next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project]
55 end
55 end
56
56
57 if watcher.watchable.respond_to?(:visible?)
57 if watcher.watchable.respond_to?(:visible?)
58 unless watcher.watchable.visible?(user)
58 unless watcher.watchable.visible?(user)
59 watcher.destroy
59 watcher.destroy
60 pruned += 1
60 pruned += 1
61 end
61 end
62 end
62 end
63 end
63 end
64 pruned
64 pruned
65 end
65 end
66 end
66 end
@@ -1,65 +1,65
1 <% roles = Role.find_all_givable %>
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
2 <% projects = Project.active.all %>
3
3
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @group.memberships.any? %>
5 <% if @group.memberships.any? %>
6 <table class="list memberships">
6 <table class="list memberships">
7 <thead><tr>
7 <thead><tr>
8 <th><%= l(:label_project) %></th>
8 <th><%= l(:label_project) %></th>
9 <th><%= l(:label_role_plural) %></th>
9 <th><%= l(:label_role_plural) %></th>
10 <th style="width:15%"></th>
10 <th style="width:15%"></th>
11 </tr></thead>
11 </tr></thead>
12 <tbody>
12 <tbody>
13 <% @group.memberships.each do |membership| %>
13 <% @group.memberships.each do |membership| %>
14 <% next if membership.new_record? %>
14 <% next if membership.new_record? %>
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
15 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <td class="project"><%=h membership.project %></td>
16 <td class="project"><%=h membership.project %></td>
17 <td class="roles">
17 <td class="roles">
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
19 <%= form_for(:membership, :remote => true,
19 <%= form_for(:membership, :remote => true,
20 :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
20 :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
22 <p><% roles.each do |role| %>
22 <p><% roles.each do |role| %>
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
24 <% end %></p>
24 <% end %></p>
25 <p><%= submit_tag l(:button_change) %>
25 <p><%= submit_tag l(:button_change) %>
26 <%= link_to_function(
26 <%= link_to_function(
27 l(:button_cancel),
27 l(:button_cancel),
28 "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;"
28 "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;"
29 ) %></p>
29 ) %></p>
30 <% end %>
30 <% end %>
31 </td>
31 </td>
32 <td class="buttons">
32 <td class="buttons">
33 <%= link_to_function(
33 <%= link_to_function(
34 l(:button_edit),
34 l(:button_edit),
35 "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;",
35 "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;",
36 :class => 'icon icon-edit'
36 :class => 'icon icon-edit'
37 ) %>
37 ) %>
38 <%= delete_link({:controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership},
38 <%= delete_link({:controller => 'groups', :action => 'destroy_membership', :id => @group, :membership_id => membership},
39 :remote => true,
39 :remote => true,
40 :method => :post) %>
40 :method => :post) %>
41 </td>
41 </td>
42 </tr>
42 </tr>
43 <% end; reset_cycle %>
43 <% end; reset_cycle %>
44 </tbody>
44 </tbody>
45 </table>
45 </table>
46 <% else %>
46 <% else %>
47 <p class="nodata"><%= l(:label_no_data) %></p>
47 <p class="nodata"><%= l(:label_no_data) %></p>
48 <% end %>
48 <% end %>
49 </div>
49 </div>
50
50
51 <div class="splitcontentright">
51 <div class="splitcontentright">
52 <% if projects.any? %>
52 <% if projects.any? %>
53 <fieldset><legend><%=l(:label_project_new)%></legend>
53 <fieldset><legend><%=l(:label_project_new)%></legend>
54 <%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %>
54 <%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %>
55 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
55 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
56 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
56 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
57 <p><%= l(:label_role_plural) %>:
57 <p><%= l(:label_role_plural) %>:
58 <% roles.each do |role| %>
58 <% roles.each do |role| %>
59 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
59 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
60 <% end %></p>
60 <% end %></p>
61 <p><%= submit_tag l(:button_add) %></p>
61 <p><%= submit_tag l(:button_add) %></p>
62 <% end %>
62 <% end %>
63 </fieldset>
63 </fieldset>
64 <% end %>
64 <% end %>
65 </div>
65 </div>
@@ -1,77 +1,77
1 <%= error_messages_for 'member' %>
1 <%= error_messages_for 'member' %>
2 <% roles = Role.find_all_givable
2 <% roles = Role.find_all_givable
3 members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %>
3 members = @project.member_principals.includes(:roles, :principal).all.sort %>
4
4
5 <div class="splitcontentleft">
5 <div class="splitcontentleft">
6 <% if members.any? %>
6 <% if members.any? %>
7 <table class="list members">
7 <table class="list members">
8 <thead><tr>
8 <thead><tr>
9 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
9 <th><%= l(:label_user) %> / <%= l(:label_group) %></th>
10 <th><%= l(:label_role_plural) %></th>
10 <th><%= l(:label_role_plural) %></th>
11 <th style="width:15%"></th>
11 <th style="width:15%"></th>
12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
12 <%= call_hook(:view_projects_settings_members_table_header, :project => @project) %>
13 </tr></thead>
13 </tr></thead>
14 <tbody>
14 <tbody>
15 <% members.each do |member| %>
15 <% members.each do |member| %>
16 <% next if member.new_record? %>
16 <% next if member.new_record? %>
17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
17 <tr id="member-<%= member.id %>" class="<%= cycle 'odd', 'even' %> member">
18 <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
18 <td class="<%= member.principal.class.name.downcase %>"><%= link_to_user member.principal %></td>
19 <td class="roles">
19 <td class="roles">
20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
20 <span id="member-<%= member.id %>-roles"><%=h member.roles.sort.collect(&:to_s).join(', ') %></span>
21 <%= form_for(member, {:as => :membership, :remote => true, :url => membership_path(member),
21 <%= form_for(member, {:as => :membership, :remote => true, :url => membership_path(member),
22 :method => :put,
22 :method => :put,
23 :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }}
23 :html => { :id => "member-#{member.id}-roles-form", :class => 'hol' }}
24 ) do |f| %>
24 ) do |f| %>
25 <p><% roles.each do |role| %>
25 <p><% roles.each do |role| %>
26 <label><%= check_box_tag 'membership[role_ids][]', role.id, member.roles.include?(role),
26 <label><%= check_box_tag 'membership[role_ids][]', role.id, member.roles.include?(role),
27 :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
27 :disabled => member.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
28 <% end %></p>
28 <% end %></p>
29 <%= hidden_field_tag 'membership[role_ids][]', '' %>
29 <%= hidden_field_tag 'membership[role_ids][]', '' %>
30 <p><%= submit_tag l(:button_change), :class => "small" %>
30 <p><%= submit_tag l(:button_change), :class => "small" %>
31 <%= link_to_function l(:button_cancel),
31 <%= link_to_function l(:button_cancel),
32 "$('#member-#{member.id}-roles').show(); $('#member-#{member.id}-roles-form').hide(); return false;"
32 "$('#member-#{member.id}-roles').show(); $('#member-#{member.id}-roles-form').hide(); return false;"
33 %></p>
33 %></p>
34 <% end %>
34 <% end %>
35 </td>
35 </td>
36 <td class="buttons">
36 <td class="buttons">
37 <%= link_to_function l(:button_edit),
37 <%= link_to_function l(:button_edit),
38 "$('#member-#{member.id}-roles').hide(); $('#member-#{member.id}-roles-form').show(); return false;",
38 "$('#member-#{member.id}-roles').hide(); $('#member-#{member.id}-roles-form').show(); return false;",
39 :class => 'icon icon-edit' %>
39 :class => 'icon icon-edit' %>
40 <%= delete_link membership_path(member),
40 <%= delete_link membership_path(member),
41 :remote => true,
41 :remote => true,
42 :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
42 :data => (!User.current.admin? && member.include?(User.current) ? {:confirm => l(:text_own_membership_delete_confirmation)} : {}) if member.deletable? %>
43 </td>
43 </td>
44 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
44 <%= call_hook(:view_projects_settings_members_table_row, { :project => @project, :member => member}) %>
45 </tr>
45 </tr>
46 <% end; reset_cycle %>
46 <% end; reset_cycle %>
47 </tbody>
47 </tbody>
48 </table>
48 </table>
49 <% else %>
49 <% else %>
50 <p class="nodata"><%= l(:label_no_data) %></p>
50 <p class="nodata"><%= l(:label_no_data) %></p>
51 <% end %>
51 <% end %>
52 </div>
52 </div>
53
53
54 <% principals = Principal.active.not_member_of(@project).all(:limit => 100, :order => 'type, login, lastname ASC') %>
54 <% principals = Principal.active.not_member_of(@project).all(:limit => 100, :order => 'type, login, lastname ASC') %>
55
55
56 <div class="splitcontentright">
56 <div class="splitcontentright">
57 <% if roles.any? && principals.any? %>
57 <% if roles.any? && principals.any? %>
58 <%= form_for(@member, {:as => :membership, :url => project_memberships_path(@project), :remote => true, :method => :post}) do |f| %>
58 <%= form_for(@member, {:as => :membership, :url => project_memberships_path(@project), :remote => true, :method => :post}) do |f| %>
59 <fieldset><legend><%=l(:label_member_new)%></legend>
59 <fieldset><legend><%=l(:label_member_new)%></legend>
60
60
61 <p><%= label_tag "principal_search", l(:label_principal_search) %><%= text_field_tag 'principal_search', nil %></p>
61 <p><%= label_tag "principal_search", l(:label_principal_search) %><%= text_field_tag 'principal_search', nil %></p>
62 <%= javascript_tag "observeSearchfield('principal_search', 'principals', '#{ escape_javascript autocomplete_project_memberships_path(@project) }')" %>
62 <%= javascript_tag "observeSearchfield('principal_search', 'principals', '#{ escape_javascript autocomplete_project_memberships_path(@project) }')" %>
63
63
64 <div id="principals">
64 <div id="principals">
65 <%= principals_check_box_tags 'membership[user_ids][]', principals %>
65 <%= principals_check_box_tags 'membership[user_ids][]', principals %>
66 </div>
66 </div>
67
67
68 <p><%= l(:label_role_plural) %>:
68 <p><%= l(:label_role_plural) %>:
69 <% roles.each do |role| %>
69 <% roles.each do |role| %>
70 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
70 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
71 <% end %></p>
71 <% end %></p>
72
72
73 <p><%= submit_tag l(:button_add), :id => 'member-add-submit' %></p>
73 <p><%= submit_tag l(:button_add), :id => 'member-add-submit' %></p>
74 </fieldset>
74 </fieldset>
75 <% end %>
75 <% end %>
76 <% end %>
76 <% end %>
77 </div>
77 </div>
@@ -1,94 +1,94
1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
2
2
3 <fieldset class="box settings enabled_scm">
3 <fieldset class="box settings enabled_scm">
4 <legend><%= l(:setting_enabled_scm) %></legend>
4 <legend><%= l(:setting_enabled_scm) %></legend>
5 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
5 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
6 <table>
6 <table>
7 <tr>
7 <tr>
8 <th></th>
8 <th></th>
9 <th><%= l(:text_scm_command) %></th>
9 <th><%= l(:text_scm_command) %></th>
10 <th><%= l(:text_scm_command_version) %></th>
10 <th><%= l(:text_scm_command_version) %></th>
11 </tr>
11 </tr>
12 <% Redmine::Scm::Base.all.collect do |choice| %>
12 <% Redmine::Scm::Base.all.collect do |choice| %>
13 <% scm_class = "Repository::#{choice}".constantize %>
13 <% scm_class = "Repository::#{choice}".constantize %>
14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
14 <% text, value = (choice.is_a?(Array) ? choice : [choice, choice]) %>
15 <% setting = :enabled_scm %>
15 <% setting = :enabled_scm %>
16 <% enabled = Setting.send(setting).include?(value) %>
16 <% enabled = Setting.send(setting).include?(value) %>
17 <tr>
17 <tr>
18 <td class="scm_name">
18 <td class="scm_name">
19 <label>
19 <label>
20 <%= check_box_tag("settings[#{setting}][]", value, enabled, :id => nil) %>
20 <%= check_box_tag("settings[#{setting}][]", value, enabled, :id => nil) %>
21 <%= text.to_s %>
21 <%= text.to_s %>
22 </label>
22 </label>
23 </td>
23 </td>
24 <td>
24 <td>
25 <% if enabled %>
25 <% if enabled %>
26 <%=
26 <%=
27 image_tag(
27 image_tag(
28 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
28 (scm_class.scm_available ? 'true.png' : 'exclamation.png'),
29 :style => "vertical-align:bottom;"
29 :style => "vertical-align:bottom;"
30 )
30 )
31 %>
31 %>
32 <%= scm_class.scm_command %>
32 <%= scm_class.scm_command %>
33 <% end %>
33 <% end %>
34 </td>
34 </td>
35 <td>
35 <td>
36 <%= scm_class.scm_version_string if enabled %>
36 <%= scm_class.scm_version_string if enabled %>
37 </td>
37 </td>
38 </tr>
38 </tr>
39 <% end %>
39 <% end %>
40 </table>
40 </table>
41 <p><em class="info"><%= l(:text_scm_config) %></em></p>
41 <p><em class="info"><%= l(:text_scm_config) %></em></p>
42 </fieldset>
42 </fieldset>
43
43
44 <div class="box tabular settings">
44 <div class="box tabular settings">
45 <p><%= setting_check_box :autofetch_changesets %></p>
45 <p><%= setting_check_box :autofetch_changesets %></p>
46
46
47 <p><%= setting_check_box :sys_api_enabled,
47 <p><%= setting_check_box :sys_api_enabled,
48 :onclick =>
48 :onclick =>
49 "if (this.checked) { $('#settings_sys_api_key').removeAttr('disabled'); } else { $('#settings_sys_api_key').attr('disabled', true); }" %></p>
49 "if (this.checked) { $('#settings_sys_api_key').removeAttr('disabled'); } else { $('#settings_sys_api_key').attr('disabled', true); }" %></p>
50
50
51 <p><%= setting_text_field :sys_api_key,
51 <p><%= setting_text_field :sys_api_key,
52 :size => 30,
52 :size => 30,
53 :id => 'settings_sys_api_key',
53 :id => 'settings_sys_api_key',
54 :disabled => !Setting.sys_api_enabled?,
54 :disabled => !Setting.sys_api_enabled?,
55 :label => :setting_mail_handler_api_key %>
55 :label => :setting_mail_handler_api_key %>
56 <%= link_to_function l(:label_generate_key),
56 <%= link_to_function l(:label_generate_key),
57 "if (!$('#settings_sys_api_key').attr('disabled')) { $('#settings_sys_api_key').val(randomKey(20)) }" %>
57 "if (!$('#settings_sys_api_key').attr('disabled')) { $('#settings_sys_api_key').val(randomKey(20)) }" %>
58 </p>
58 </p>
59
59
60 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
60 <p><%= setting_text_field :repository_log_display_limit, :size => 6 %></p>
61 </div>
61 </div>
62
62
63 <fieldset class="box tabular settings">
63 <fieldset class="box tabular settings">
64 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
64 <legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
65 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
65 <p><%= setting_text_field :commit_ref_keywords, :size => 30 %>
66 <em class="info"><%= l(:text_comma_separated) %></em></p>
66 <em class="info"><%= l(:text_comma_separated) %></em></p>
67
67
68 <p><%= setting_text_field :commit_fix_keywords, :size => 30 %>
68 <p><%= setting_text_field :commit_fix_keywords, :size => 30 %>
69 &nbsp;<%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id,
69 &nbsp;<%= l(:label_applied_status) %>: <%= setting_select :commit_fix_status_id,
70 [["", 0]] +
70 [["", 0]] +
71 IssueStatus.find(:all).collect{
71 IssueStatus.sorted.all.collect{
72 |status| [status.name, status.id.to_s]
72 |status| [status.name, status.id.to_s]
73 },
73 },
74 :label => false %>
74 :label => false %>
75 &nbsp;<%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio,
75 &nbsp;<%= l(:field_done_ratio) %>: <%= setting_select :commit_fix_done_ratio,
76 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
76 (0..10).to_a.collect {|r| ["#{r*10} %", "#{r*10}"] },
77 :blank => :label_no_change_option,
77 :blank => :label_no_change_option,
78 :label => false %>
78 :label => false %>
79 <em class="info"><%= l(:text_comma_separated) %></em></p>
79 <em class="info"><%= l(:text_comma_separated) %></em></p>
80
80
81 <p><%= setting_check_box :commit_cross_project_ref %></p>
81 <p><%= setting_check_box :commit_cross_project_ref %></p>
82
82
83 <p><%= setting_check_box :commit_logtime_enabled,
83 <p><%= setting_check_box :commit_logtime_enabled,
84 :onclick =>
84 :onclick =>
85 "if (this.checked) { $('#settings_commit_logtime_activity_id').removeAttr('disabled'); } else { $('#settings_commit_logtime_activity_id').attr('disabled', true); }"%></p>
85 "if (this.checked) { $('#settings_commit_logtime_activity_id').removeAttr('disabled'); } else { $('#settings_commit_logtime_activity_id').attr('disabled', true); }"%></p>
86
86
87 <p><%= setting_select :commit_logtime_activity_id,
87 <p><%= setting_select :commit_logtime_activity_id,
88 [[l(:label_default), 0]] +
88 [[l(:label_default), 0]] +
89 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
89 TimeEntryActivity.shared.active.collect{|activity| [activity.name, activity.id.to_s]},
90 :disabled => !Setting.commit_logtime_enabled?%></p>
90 :disabled => !Setting.commit_logtime_enabled?%></p>
91 </fieldset>
91 </fieldset>
92
92
93 <%= submit_tag l(:button_save) %>
93 <%= submit_tag l(:button_save) %>
94 <% end %>
94 <% end %>
@@ -1,67 +1,67
1 <% roles = Role.find_all_givable %>
1 <% roles = Role.find_all_givable %>
2 <% projects = Project.active.find(:all, :order => 'lft') %>
2 <% projects = Project.active.all %>
3
3
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
5 <% if @user.memberships.any? %>
5 <% if @user.memberships.any? %>
6 <table class="list memberships">
6 <table class="list memberships">
7 <thead><tr>
7 <thead><tr>
8 <th><%= l(:label_project) %></th>
8 <th><%= l(:label_project) %></th>
9 <th><%= l(:label_role_plural) %></th>
9 <th><%= l(:label_role_plural) %></th>
10 <th style="width:15%"></th>
10 <th style="width:15%"></th>
11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
11 <%= call_hook(:view_users_memberships_table_header, :user => @user )%>
12 </tr></thead>
12 </tr></thead>
13 <tbody>
13 <tbody>
14 <% @user.memberships.each do |membership| %>
14 <% @user.memberships.each do |membership| %>
15 <% next if membership.new_record? %>
15 <% next if membership.new_record? %>
16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
16 <tr id="member-<%= membership.id %>" class="<%= cycle 'odd', 'even' %> class">
17 <td class="project">
17 <td class="project">
18 <%= link_to_project membership.project %>
18 <%= link_to_project membership.project %>
19 </td>
19 </td>
20 <td class="roles">
20 <td class="roles">
21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
22 <%= form_for(:membership, :remote => true,
22 <%= form_for(:membership, :remote => true,
23 :url => user_membership_path(@user, membership), :method => :put,
23 :url => user_membership_path(@user, membership), :method => :put,
24 :html => {:id => "member-#{membership.id}-roles-form",
24 :html => {:id => "member-#{membership.id}-roles-form",
25 :style => 'display:none;'}) do %>
25 :style => 'display:none;'}) do %>
26 <p><% roles.each do |role| %>
26 <p><% roles.each do |role| %>
27 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
27 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
28 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
28 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
29 <% end %></p>
29 <% end %></p>
30 <%= hidden_field_tag 'membership[role_ids][]', '' %>
30 <%= hidden_field_tag 'membership[role_ids][]', '' %>
31 <p><%= submit_tag l(:button_change) %>
31 <p><%= submit_tag l(:button_change) %>
32 <%= link_to_function l(:button_cancel),
32 <%= link_to_function l(:button_cancel),
33 "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;"
33 "$('#member-#{membership.id}-roles').show(); $('#member-#{membership.id}-roles-form').hide(); return false;"
34 %></p>
34 %></p>
35 <% end %>
35 <% end %>
36 </td>
36 </td>
37 <td class="buttons">
37 <td class="buttons">
38 <%= link_to_function l(:button_edit),
38 <%= link_to_function l(:button_edit),
39 "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;",
39 "$('#member-#{membership.id}-roles').hide(); $('#member-#{membership.id}-roles-form').show(); return false;",
40 :class => 'icon icon-edit'
40 :class => 'icon icon-edit'
41 %>
41 %>
42 <%= delete_link user_membership_path(@user, membership), :remote => true if membership.deletable? %>
42 <%= delete_link user_membership_path(@user, membership), :remote => true if membership.deletable? %>
43 </td>
43 </td>
44 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
44 <%= call_hook(:view_users_memberships_table_row, :user => @user, :membership => membership, :roles => roles, :projects => projects )%>
45 </tr>
45 </tr>
46 <% end; reset_cycle %>
46 <% end; reset_cycle %>
47 </tbody>
47 </tbody>
48 </table>
48 </table>
49 <% else %>
49 <% else %>
50 <p class="nodata"><%= l(:label_no_data) %></p>
50 <p class="nodata"><%= l(:label_no_data) %></p>
51 <% end %>
51 <% end %>
52 </div>
52 </div>
53
53
54 <div class="splitcontentright">
54 <div class="splitcontentright">
55 <% if projects.any? %>
55 <% if projects.any? %>
56 <fieldset><legend><%=l(:label_project_new)%></legend>
56 <fieldset><legend><%=l(:label_project_new)%></legend>
57 <%= form_for(:membership, :remote => true, :url => user_memberships_path(@user)) do %>
57 <%= form_for(:membership, :remote => true, :url => user_memberships_path(@user)) do %>
58 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
58 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
59 <p><%= l(:label_role_plural) %>:
59 <p><%= l(:label_role_plural) %>:
60 <% roles.each do |role| %>
60 <% roles.each do |role| %>
61 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
61 <label><%= check_box_tag 'membership[role_ids][]', role.id %> <%=h role %></label>
62 <% end %></p>
62 <% end %></p>
63 <p><%= submit_tag l(:button_add) %></p>
63 <p><%= submit_tag l(:button_add) %></p>
64 <% end %>
64 <% end %>
65 </fieldset>
65 </fieldset>
66 <% end %>
66 <% end %>
67 </div>
67 </div>
@@ -1,161 +1,160
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Customizable
20 module Customizable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_customizable(options = {})
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
28 cattr_accessor :customizable_options
29 self.customizable_options = options
29 self.customizable_options = options
30 has_many :custom_values, :as => :customized,
30 has_many :custom_values, :as => :customized,
31 :include => :custom_field,
31 :include => :custom_field,
32 :order => "#{CustomField.table_name}.position",
32 :order => "#{CustomField.table_name}.position",
33 :dependent => :delete_all,
33 :dependent => :delete_all,
34 :validate => false
34 :validate => false
35 send :include, Redmine::Acts::Customizable::InstanceMethods
35 send :include, Redmine::Acts::Customizable::InstanceMethods
36 validate :validate_custom_field_values
36 validate :validate_custom_field_values
37 after_save :save_custom_field_values
37 after_save :save_custom_field_values
38 end
38 end
39 end
39 end
40
40
41 module InstanceMethods
41 module InstanceMethods
42 def self.included(base)
42 def self.included(base)
43 base.extend ClassMethods
43 base.extend ClassMethods
44 end
44 end
45
45
46 def available_custom_fields
46 def available_custom_fields
47 CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
47 CustomField.where("type = '#{self.class.name}CustomField'").sorted.all
48 :order => 'position')
49 end
48 end
50
49
51 # Sets the values of the object's custom fields
50 # Sets the values of the object's custom fields
52 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
51 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
53 def custom_fields=(values)
52 def custom_fields=(values)
54 values_to_hash = values.inject({}) do |hash, v|
53 values_to_hash = values.inject({}) do |hash, v|
55 v = v.stringify_keys
54 v = v.stringify_keys
56 if v['id'] && v.has_key?('value')
55 if v['id'] && v.has_key?('value')
57 hash[v['id']] = v['value']
56 hash[v['id']] = v['value']
58 end
57 end
59 hash
58 hash
60 end
59 end
61 self.custom_field_values = values_to_hash
60 self.custom_field_values = values_to_hash
62 end
61 end
63
62
64 # Sets the values of the object's custom fields
63 # Sets the values of the object's custom fields
65 # values is a hash like {'1' => 'foo', 2 => 'bar'}
64 # values is a hash like {'1' => 'foo', 2 => 'bar'}
66 def custom_field_values=(values)
65 def custom_field_values=(values)
67 values = values.stringify_keys
66 values = values.stringify_keys
68
67
69 custom_field_values.each do |custom_field_value|
68 custom_field_values.each do |custom_field_value|
70 key = custom_field_value.custom_field_id.to_s
69 key = custom_field_value.custom_field_id.to_s
71 if values.has_key?(key)
70 if values.has_key?(key)
72 value = values[key]
71 value = values[key]
73 if value.is_a?(Array)
72 if value.is_a?(Array)
74 value = value.reject(&:blank?).uniq
73 value = value.reject(&:blank?).uniq
75 if value.empty?
74 if value.empty?
76 value << ''
75 value << ''
77 end
76 end
78 end
77 end
79 custom_field_value.value = value
78 custom_field_value.value = value
80 end
79 end
81 end
80 end
82 @custom_field_values_changed = true
81 @custom_field_values_changed = true
83 end
82 end
84
83
85 def custom_field_values
84 def custom_field_values
86 @custom_field_values ||= available_custom_fields.collect do |field|
85 @custom_field_values ||= available_custom_fields.collect do |field|
87 x = CustomFieldValue.new
86 x = CustomFieldValue.new
88 x.custom_field = field
87 x.custom_field = field
89 x.customized = self
88 x.customized = self
90 if field.multiple?
89 if field.multiple?
91 values = custom_values.select { |v| v.custom_field == field }
90 values = custom_values.select { |v| v.custom_field == field }
92 if values.empty?
91 if values.empty?
93 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
92 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
94 end
93 end
95 x.value = values.map(&:value)
94 x.value = values.map(&:value)
96 else
95 else
97 cv = custom_values.detect { |v| v.custom_field == field }
96 cv = custom_values.detect { |v| v.custom_field == field }
98 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
97 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
99 x.value = cv.value
98 x.value = cv.value
100 end
99 end
101 x
100 x
102 end
101 end
103 end
102 end
104
103
105 def visible_custom_field_values
104 def visible_custom_field_values
106 custom_field_values.select(&:visible?)
105 custom_field_values.select(&:visible?)
107 end
106 end
108
107
109 def custom_field_values_changed?
108 def custom_field_values_changed?
110 @custom_field_values_changed == true
109 @custom_field_values_changed == true
111 end
110 end
112
111
113 def custom_value_for(c)
112 def custom_value_for(c)
114 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
113 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
115 custom_values.detect {|v| v.custom_field_id == field_id }
114 custom_values.detect {|v| v.custom_field_id == field_id }
116 end
115 end
117
116
118 def custom_field_value(c)
117 def custom_field_value(c)
119 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
118 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
120 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
119 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
121 end
120 end
122
121
123 def validate_custom_field_values
122 def validate_custom_field_values
124 if new_record? || custom_field_values_changed?
123 if new_record? || custom_field_values_changed?
125 custom_field_values.each(&:validate_value)
124 custom_field_values.each(&:validate_value)
126 end
125 end
127 end
126 end
128
127
129 def save_custom_field_values
128 def save_custom_field_values
130 target_custom_values = []
129 target_custom_values = []
131 custom_field_values.each do |custom_field_value|
130 custom_field_values.each do |custom_field_value|
132 if custom_field_value.value.is_a?(Array)
131 if custom_field_value.value.is_a?(Array)
133 custom_field_value.value.each do |v|
132 custom_field_value.value.each do |v|
134 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
133 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
135 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
134 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
136 target_custom_values << target
135 target_custom_values << target
137 end
136 end
138 else
137 else
139 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
138 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
140 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
139 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
141 target.value = custom_field_value.value
140 target.value = custom_field_value.value
142 target_custom_values << target
141 target_custom_values << target
143 end
142 end
144 end
143 end
145 self.custom_values = target_custom_values
144 self.custom_values = target_custom_values
146 custom_values.each(&:save)
145 custom_values.each(&:save)
147 @custom_field_values_changed = false
146 @custom_field_values_changed = false
148 true
147 true
149 end
148 end
150
149
151 def reset_custom_values!
150 def reset_custom_values!
152 @custom_field_values = nil
151 @custom_field_values = nil
153 @custom_field_values_changed = true
152 @custom_field_values_changed = true
154 end
153 end
155
154
156 module ClassMethods
155 module ClassMethods
157 end
156 end
158 end
157 end
159 end
158 end
160 end
159 end
161 end
160 end
@@ -1,185 +1,185
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 module Redmine
18 module Redmine
19 module DefaultData
19 module DefaultData
20 class DataAlreadyLoaded < Exception; end
20 class DataAlreadyLoaded < Exception; end
21
21
22 module Loader
22 module Loader
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 class << self
25 class << self
26 # Returns true if no data is already loaded in the database
26 # Returns true if no data is already loaded in the database
27 # otherwise false
27 # otherwise false
28 def no_data?
28 def no_data?
29 !Role.find(:first, :conditions => {:builtin => 0}) &&
29 !Role.find(:first, :conditions => {:builtin => 0}) &&
30 !Tracker.find(:first) &&
30 !Tracker.find(:first) &&
31 !IssueStatus.find(:first) &&
31 !IssueStatus.find(:first) &&
32 !Enumeration.find(:first)
32 !Enumeration.find(:first)
33 end
33 end
34
34
35 # Loads the default data
35 # Loads the default data
36 # Raises a RecordNotSaved exception if something goes wrong
36 # Raises a RecordNotSaved exception if something goes wrong
37 def load(lang=nil)
37 def load(lang=nil)
38 raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
38 raise DataAlreadyLoaded.new("Some configuration data is already loaded.") unless no_data?
39 set_language_if_valid(lang)
39 set_language_if_valid(lang)
40
40
41 Role.transaction do
41 Role.transaction do
42 # Roles
42 # Roles
43 manager = Role.create! :name => l(:default_role_manager),
43 manager = Role.create! :name => l(:default_role_manager),
44 :issues_visibility => 'all',
44 :issues_visibility => 'all',
45 :position => 1
45 :position => 1
46 manager.permissions = manager.setable_permissions.collect {|p| p.name}
46 manager.permissions = manager.setable_permissions.collect {|p| p.name}
47 manager.save!
47 manager.save!
48
48
49 developer = Role.create! :name => l(:default_role_developer),
49 developer = Role.create! :name => l(:default_role_developer),
50 :position => 2,
50 :position => 2,
51 :permissions => [:manage_versions,
51 :permissions => [:manage_versions,
52 :manage_categories,
52 :manage_categories,
53 :view_issues,
53 :view_issues,
54 :add_issues,
54 :add_issues,
55 :edit_issues,
55 :edit_issues,
56 :view_private_notes,
56 :view_private_notes,
57 :set_notes_private,
57 :set_notes_private,
58 :manage_issue_relations,
58 :manage_issue_relations,
59 :manage_subtasks,
59 :manage_subtasks,
60 :add_issue_notes,
60 :add_issue_notes,
61 :save_queries,
61 :save_queries,
62 :view_gantt,
62 :view_gantt,
63 :view_calendar,
63 :view_calendar,
64 :log_time,
64 :log_time,
65 :view_time_entries,
65 :view_time_entries,
66 :comment_news,
66 :comment_news,
67 :view_documents,
67 :view_documents,
68 :view_wiki_pages,
68 :view_wiki_pages,
69 :view_wiki_edits,
69 :view_wiki_edits,
70 :edit_wiki_pages,
70 :edit_wiki_pages,
71 :delete_wiki_pages,
71 :delete_wiki_pages,
72 :add_messages,
72 :add_messages,
73 :edit_own_messages,
73 :edit_own_messages,
74 :view_files,
74 :view_files,
75 :manage_files,
75 :manage_files,
76 :browse_repository,
76 :browse_repository,
77 :view_changesets,
77 :view_changesets,
78 :commit_access,
78 :commit_access,
79 :manage_related_issues]
79 :manage_related_issues]
80
80
81 reporter = Role.create! :name => l(:default_role_reporter),
81 reporter = Role.create! :name => l(:default_role_reporter),
82 :position => 3,
82 :position => 3,
83 :permissions => [:view_issues,
83 :permissions => [:view_issues,
84 :add_issues,
84 :add_issues,
85 :add_issue_notes,
85 :add_issue_notes,
86 :save_queries,
86 :save_queries,
87 :view_gantt,
87 :view_gantt,
88 :view_calendar,
88 :view_calendar,
89 :log_time,
89 :log_time,
90 :view_time_entries,
90 :view_time_entries,
91 :comment_news,
91 :comment_news,
92 :view_documents,
92 :view_documents,
93 :view_wiki_pages,
93 :view_wiki_pages,
94 :view_wiki_edits,
94 :view_wiki_edits,
95 :add_messages,
95 :add_messages,
96 :edit_own_messages,
96 :edit_own_messages,
97 :view_files,
97 :view_files,
98 :browse_repository,
98 :browse_repository,
99 :view_changesets]
99 :view_changesets]
100
100
101 Role.non_member.update_attribute :permissions, [:view_issues,
101 Role.non_member.update_attribute :permissions, [:view_issues,
102 :add_issues,
102 :add_issues,
103 :add_issue_notes,
103 :add_issue_notes,
104 :save_queries,
104 :save_queries,
105 :view_gantt,
105 :view_gantt,
106 :view_calendar,
106 :view_calendar,
107 :view_time_entries,
107 :view_time_entries,
108 :comment_news,
108 :comment_news,
109 :view_documents,
109 :view_documents,
110 :view_wiki_pages,
110 :view_wiki_pages,
111 :view_wiki_edits,
111 :view_wiki_edits,
112 :add_messages,
112 :add_messages,
113 :view_files,
113 :view_files,
114 :browse_repository,
114 :browse_repository,
115 :view_changesets]
115 :view_changesets]
116
116
117 Role.anonymous.update_attribute :permissions, [:view_issues,
117 Role.anonymous.update_attribute :permissions, [:view_issues,
118 :view_gantt,
118 :view_gantt,
119 :view_calendar,
119 :view_calendar,
120 :view_time_entries,
120 :view_time_entries,
121 :view_documents,
121 :view_documents,
122 :view_wiki_pages,
122 :view_wiki_pages,
123 :view_wiki_edits,
123 :view_wiki_edits,
124 :view_files,
124 :view_files,
125 :browse_repository,
125 :browse_repository,
126 :view_changesets]
126 :view_changesets]
127
127
128 # Trackers
128 # Trackers
129 Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
129 Tracker.create!(:name => l(:default_tracker_bug), :is_in_chlog => true, :is_in_roadmap => false, :position => 1)
130 Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
130 Tracker.create!(:name => l(:default_tracker_feature), :is_in_chlog => true, :is_in_roadmap => true, :position => 2)
131 Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
131 Tracker.create!(:name => l(:default_tracker_support), :is_in_chlog => false, :is_in_roadmap => false, :position => 3)
132
132
133 # Issue statuses
133 # Issue statuses
134 new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
134 new = IssueStatus.create!(:name => l(:default_issue_status_new), :is_closed => false, :is_default => true, :position => 1)
135 in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
135 in_progress = IssueStatus.create!(:name => l(:default_issue_status_in_progress), :is_closed => false, :is_default => false, :position => 2)
136 resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
136 resolved = IssueStatus.create!(:name => l(:default_issue_status_resolved), :is_closed => false, :is_default => false, :position => 3)
137 feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
137 feedback = IssueStatus.create!(:name => l(:default_issue_status_feedback), :is_closed => false, :is_default => false, :position => 4)
138 closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
138 closed = IssueStatus.create!(:name => l(:default_issue_status_closed), :is_closed => true, :is_default => false, :position => 5)
139 rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
139 rejected = IssueStatus.create!(:name => l(:default_issue_status_rejected), :is_closed => true, :is_default => false, :position => 6)
140
140
141 # Workflow
141 # Workflow
142 Tracker.find(:all).each { |t|
142 Tracker.all.each { |t|
143 IssueStatus.find(:all).each { |os|
143 IssueStatus.all.each { |os|
144 IssueStatus.find(:all).each { |ns|
144 IssueStatus.all.each { |ns|
145 WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
145 WorkflowTransition.create!(:tracker_id => t.id, :role_id => manager.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
146 }
146 }
147 }
147 }
148 }
148 }
149
149
150 Tracker.find(:all).each { |t|
150 Tracker.all.each { |t|
151 [new, in_progress, resolved, feedback].each { |os|
151 [new, in_progress, resolved, feedback].each { |os|
152 [in_progress, resolved, feedback, closed].each { |ns|
152 [in_progress, resolved, feedback, closed].each { |ns|
153 WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
153 WorkflowTransition.create!(:tracker_id => t.id, :role_id => developer.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
154 }
154 }
155 }
155 }
156 }
156 }
157
157
158 Tracker.find(:all).each { |t|
158 Tracker.all.each { |t|
159 [new, in_progress, resolved, feedback].each { |os|
159 [new, in_progress, resolved, feedback].each { |os|
160 [closed].each { |ns|
160 [closed].each { |ns|
161 WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
161 WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => os.id, :new_status_id => ns.id) unless os == ns
162 }
162 }
163 }
163 }
164 WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
164 WorkflowTransition.create!(:tracker_id => t.id, :role_id => reporter.id, :old_status_id => resolved.id, :new_status_id => feedback.id)
165 }
165 }
166
166
167 # Enumerations
167 # Enumerations
168 IssuePriority.create!(:name => l(:default_priority_low), :position => 1)
168 IssuePriority.create!(:name => l(:default_priority_low), :position => 1)
169 IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true)
169 IssuePriority.create!(:name => l(:default_priority_normal), :position => 2, :is_default => true)
170 IssuePriority.create!(:name => l(:default_priority_high), :position => 3)
170 IssuePriority.create!(:name => l(:default_priority_high), :position => 3)
171 IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4)
171 IssuePriority.create!(:name => l(:default_priority_urgent), :position => 4)
172 IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5)
172 IssuePriority.create!(:name => l(:default_priority_immediate), :position => 5)
173
173
174 DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1)
174 DocumentCategory.create!(:name => l(:default_doc_category_user), :position => 1)
175 DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2)
175 DocumentCategory.create!(:name => l(:default_doc_category_tech), :position => 2)
176
176
177 TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1)
177 TimeEntryActivity.create!(:name => l(:default_activity_design), :position => 1)
178 TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2)
178 TimeEntryActivity.create!(:name => l(:default_activity_development), :position => 2)
179 end
179 end
180 true
180 true
181 end
181 end
182 end
182 end
183 end
183 end
184 end
184 end
185 end
185 end
@@ -1,164 +1,164
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 module Redmine
18 module Redmine
19 module Helpers
19 module Helpers
20 class TimeReport
20 class TimeReport
21 attr_reader :criteria, :columns, :from, :to, :hours, :total_hours, :periods
21 attr_reader :criteria, :columns, :from, :to, :hours, :total_hours, :periods
22
22
23 def initialize(project, issue, criteria, columns, from, to)
23 def initialize(project, issue, criteria, columns, from, to)
24 @project = project
24 @project = project
25 @issue = issue
25 @issue = issue
26
26
27 @criteria = criteria || []
27 @criteria = criteria || []
28 @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
28 @criteria = @criteria.select{|criteria| available_criteria.has_key? criteria}
29 @criteria.uniq!
29 @criteria.uniq!
30 @criteria = @criteria[0,3]
30 @criteria = @criteria[0,3]
31
31
32 @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
32 @columns = (columns && %w(year month week day).include?(columns)) ? columns : 'month'
33 @from = from
33 @from = from
34 @to = to
34 @to = to
35
35
36 run
36 run
37 end
37 end
38
38
39 def available_criteria
39 def available_criteria
40 @available_criteria || load_available_criteria
40 @available_criteria || load_available_criteria
41 end
41 end
42
42
43 private
43 private
44
44
45 def run
45 def run
46 unless @criteria.empty?
46 unless @criteria.empty?
47 scope = TimeEntry.visible.spent_between(@from, @to)
47 scope = TimeEntry.visible.spent_between(@from, @to)
48 if @issue
48 if @issue
49 scope = scope.on_issue(@issue)
49 scope = scope.on_issue(@issue)
50 elsif @project
50 elsif @project
51 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
51 scope = scope.on_project(@project, Setting.display_subprojects_issues?)
52 end
52 end
53 time_columns = %w(tyear tmonth tweek spent_on)
53 time_columns = %w(tyear tmonth tweek spent_on)
54 @hours = []
54 @hours = []
55 scope.sum(:hours, :include => :issue, :group => @criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).each do |hash, hours|
55 scope.sum(:hours, :include => :issue, :group => @criteria.collect{|criteria| @available_criteria[criteria][:sql]} + time_columns).each do |hash, hours|
56 h = {'hours' => hours}
56 h = {'hours' => hours}
57 (@criteria + time_columns).each_with_index do |name, i|
57 (@criteria + time_columns).each_with_index do |name, i|
58 h[name] = hash[i]
58 h[name] = hash[i]
59 end
59 end
60 @hours << h
60 @hours << h
61 end
61 end
62
62
63 @hours.each do |row|
63 @hours.each do |row|
64 case @columns
64 case @columns
65 when 'year'
65 when 'year'
66 row['year'] = row['tyear']
66 row['year'] = row['tyear']
67 when 'month'
67 when 'month'
68 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
68 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
69 when 'week'
69 when 'week'
70 row['week'] = "#{row['tyear']}-#{row['tweek']}"
70 row['week'] = "#{row['tyear']}-#{row['tweek']}"
71 when 'day'
71 when 'day'
72 row['day'] = "#{row['spent_on']}"
72 row['day'] = "#{row['spent_on']}"
73 end
73 end
74 end
74 end
75
75
76 if @from.nil?
76 if @from.nil?
77 min = @hours.collect {|row| row['spent_on']}.min
77 min = @hours.collect {|row| row['spent_on']}.min
78 @from = min ? min.to_date : Date.today
78 @from = min ? min.to_date : Date.today
79 end
79 end
80
80
81 if @to.nil?
81 if @to.nil?
82 max = @hours.collect {|row| row['spent_on']}.max
82 max = @hours.collect {|row| row['spent_on']}.max
83 @to = max ? max.to_date : Date.today
83 @to = max ? max.to_date : Date.today
84 end
84 end
85
85
86 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
86 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
87
87
88 @periods = []
88 @periods = []
89 # Date#at_beginning_of_ not supported in Rails 1.2.x
89 # Date#at_beginning_of_ not supported in Rails 1.2.x
90 date_from = @from.to_time
90 date_from = @from.to_time
91 # 100 columns max
91 # 100 columns max
92 while date_from <= @to.to_time && @periods.length < 100
92 while date_from <= @to.to_time && @periods.length < 100
93 case @columns
93 case @columns
94 when 'year'
94 when 'year'
95 @periods << "#{date_from.year}"
95 @periods << "#{date_from.year}"
96 date_from = (date_from + 1.year).at_beginning_of_year
96 date_from = (date_from + 1.year).at_beginning_of_year
97 when 'month'
97 when 'month'
98 @periods << "#{date_from.year}-#{date_from.month}"
98 @periods << "#{date_from.year}-#{date_from.month}"
99 date_from = (date_from + 1.month).at_beginning_of_month
99 date_from = (date_from + 1.month).at_beginning_of_month
100 when 'week'
100 when 'week'
101 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
101 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
102 date_from = (date_from + 7.day).at_beginning_of_week
102 date_from = (date_from + 7.day).at_beginning_of_week
103 when 'day'
103 when 'day'
104 @periods << "#{date_from.to_date}"
104 @periods << "#{date_from.to_date}"
105 date_from = date_from + 1.day
105 date_from = date_from + 1.day
106 end
106 end
107 end
107 end
108 end
108 end
109 end
109 end
110
110
111 def load_available_criteria
111 def load_available_criteria
112 @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
112 @available_criteria = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
113 :klass => Project,
113 :klass => Project,
114 :label => :label_project},
114 :label => :label_project},
115 'status' => {:sql => "#{Issue.table_name}.status_id",
115 'status' => {:sql => "#{Issue.table_name}.status_id",
116 :klass => IssueStatus,
116 :klass => IssueStatus,
117 :label => :field_status},
117 :label => :field_status},
118 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
118 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
119 :klass => Version,
119 :klass => Version,
120 :label => :label_version},
120 :label => :label_version},
121 'category' => {:sql => "#{Issue.table_name}.category_id",
121 'category' => {:sql => "#{Issue.table_name}.category_id",
122 :klass => IssueCategory,
122 :klass => IssueCategory,
123 :label => :field_category},
123 :label => :field_category},
124 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
124 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
125 :klass => User,
125 :klass => User,
126 :label => :label_member},
126 :label => :label_member},
127 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
127 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
128 :klass => Tracker,
128 :klass => Tracker,
129 :label => :label_tracker},
129 :label => :label_tracker},
130 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
130 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
131 :klass => TimeEntryActivity,
131 :klass => TimeEntryActivity,
132 :label => :label_activity},
132 :label => :label_activity},
133 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
133 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
134 :klass => Issue,
134 :klass => Issue,
135 :label => :label_issue}
135 :label => :label_issue}
136 }
136 }
137
137
138 # Add list and boolean custom fields as available criteria
138 # Add list and boolean custom fields as available criteria
139 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
139 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
140 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
140 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
141 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id ORDER BY c.value LIMIT 1)",
141 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id ORDER BY c.value LIMIT 1)",
142 :format => cf.field_format,
142 :format => cf.field_format,
143 :label => cf.name}
143 :label => cf.name}
144 end if @project
144 end if @project
145
145
146 # Add list and boolean time entry custom fields
146 # Add list and boolean time entry custom fields
147 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
147 TimeEntryCustomField.all.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
148 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id ORDER BY c.value LIMIT 1)",
148 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id ORDER BY c.value LIMIT 1)",
149 :format => cf.field_format,
149 :format => cf.field_format,
150 :label => cf.name}
150 :label => cf.name}
151 end
151 end
152
152
153 # Add list and boolean time entry activity custom fields
153 # Add list and boolean time entry activity custom fields
154 TimeEntryActivityCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
154 TimeEntryActivityCustomField.all.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
155 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id ORDER BY c.value LIMIT 1)",
155 @available_criteria["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Enumeration' AND c.customized_id = #{TimeEntry.table_name}.activity_id ORDER BY c.value LIMIT 1)",
156 :format => cf.field_format,
156 :format => cf.field_format,
157 :label => cf.name}
157 :label => cf.name}
158 end
158 end
159
159
160 @available_criteria
160 @available_criteria
161 end
161 end
162 end
162 end
163 end
163 end
164 end
164 end
@@ -1,511 +1,511
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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.where(:builtin => 0).order('position ASC').all
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 self.table_name = :mantis_user_table
87 self.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
91 @firstname
92 end
92 end
93
93
94 def lastname
94 def lastname
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
95 @lastname = realname.blank? ? '-' : realname.split[1..-1].join(' ')[0..29]
96 @lastname = '-' if @lastname.blank?
96 @lastname = '-' if @lastname.blank?
97 @lastname
97 @lastname
98 end
98 end
99
99
100 def email
100 def email
101 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) &&
102 !User.find_by_mail(read_attribute(:email))
102 !User.find_by_mail(read_attribute(:email))
103 @email = read_attribute(:email)
103 @email = read_attribute(:email)
104 else
104 else
105 @email = "#{username}@foo.bar"
105 @email = "#{username}@foo.bar"
106 end
106 end
107 end
107 end
108
108
109 def username
109 def username
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
110 read_attribute(:username)[0..29].gsub(/[^a-zA-Z0-9_\-@\.]/, '-')
111 end
111 end
112 end
112 end
113
113
114 class MantisProject < ActiveRecord::Base
114 class MantisProject < ActiveRecord::Base
115 self.table_name = :mantis_project_table
115 self.table_name = :mantis_project_table
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
116 has_many :versions, :class_name => "MantisVersion", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
117 has_many :categories, :class_name => "MantisCategory", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
118 has_many :news, :class_name => "MantisNews", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
119 has_many :members, :class_name => "MantisProjectUser", :foreign_key => :project_id
120
120
121 def identifier
121 def identifier
122 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)
123 end
123 end
124 end
124 end
125
125
126 class MantisVersion < ActiveRecord::Base
126 class MantisVersion < ActiveRecord::Base
127 self.table_name = :mantis_project_version_table
127 self.table_name = :mantis_project_version_table
128
128
129 def version
129 def version
130 read_attribute(:version)[0..29]
130 read_attribute(:version)[0..29]
131 end
131 end
132
132
133 def description
133 def description
134 read_attribute(:description)[0..254]
134 read_attribute(:description)[0..254]
135 end
135 end
136 end
136 end
137
137
138 class MantisCategory < ActiveRecord::Base
138 class MantisCategory < ActiveRecord::Base
139 self.table_name = :mantis_project_category_table
139 self.table_name = :mantis_project_category_table
140 end
140 end
141
141
142 class MantisProjectUser < ActiveRecord::Base
142 class MantisProjectUser < ActiveRecord::Base
143 self.table_name = :mantis_project_user_list_table
143 self.table_name = :mantis_project_user_list_table
144 end
144 end
145
145
146 class MantisBug < ActiveRecord::Base
146 class MantisBug < ActiveRecord::Base
147 self.table_name = :mantis_bug_table
147 self.table_name = :mantis_bug_table
148 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
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
149 has_many :bug_notes, :class_name => "MantisBugNote", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
150 has_many :bug_files, :class_name => "MantisBugFile", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
151 has_many :bug_monitors, :class_name => "MantisBugMonitor", :foreign_key => :bug_id
152 end
152 end
153
153
154 class MantisBugText < ActiveRecord::Base
154 class MantisBugText < ActiveRecord::Base
155 self.table_name = :mantis_bug_text_table
155 self.table_name = :mantis_bug_text_table
156
156
157 # Adds Mantis steps_to_reproduce and additional_information fields
157 # Adds Mantis steps_to_reproduce and additional_information fields
158 # to description if any
158 # to description if any
159 def full_description
159 def full_description
160 full_description = description
160 full_description = description
161 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?
162 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?
163 full_description
163 full_description
164 end
164 end
165 end
165 end
166
166
167 class MantisBugNote < ActiveRecord::Base
167 class MantisBugNote < ActiveRecord::Base
168 self.table_name = :mantis_bugnote_table
168 self.table_name = :mantis_bugnote_table
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
169 belongs_to :bug, :class_name => "MantisBug", :foreign_key => :bug_id
170 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
171 end
171 end
172
172
173 class MantisBugNoteText < ActiveRecord::Base
173 class MantisBugNoteText < ActiveRecord::Base
174 self.table_name = :mantis_bugnote_text_table
174 self.table_name = :mantis_bugnote_text_table
175 end
175 end
176
176
177 class MantisBugFile < ActiveRecord::Base
177 class MantisBugFile < ActiveRecord::Base
178 self.table_name = :mantis_bug_file_table
178 self.table_name = :mantis_bug_file_table
179
179
180 def size
180 def size
181 filesize
181 filesize
182 end
182 end
183
183
184 def original_filename
184 def original_filename
185 MantisMigrate.encode(filename)
185 MantisMigrate.encode(filename)
186 end
186 end
187
187
188 def content_type
188 def content_type
189 file_type
189 file_type
190 end
190 end
191
191
192 def read(*args)
192 def read(*args)
193 if @read_finished
193 if @read_finished
194 nil
194 nil
195 else
195 else
196 @read_finished = true
196 @read_finished = true
197 content
197 content
198 end
198 end
199 end
199 end
200 end
200 end
201
201
202 class MantisBugRelationship < ActiveRecord::Base
202 class MantisBugRelationship < ActiveRecord::Base
203 self.table_name = :mantis_bug_relationship_table
203 self.table_name = :mantis_bug_relationship_table
204 end
204 end
205
205
206 class MantisBugMonitor < ActiveRecord::Base
206 class MantisBugMonitor < ActiveRecord::Base
207 self.table_name = :mantis_bug_monitor_table
207 self.table_name = :mantis_bug_monitor_table
208 end
208 end
209
209
210 class MantisNews < ActiveRecord::Base
210 class MantisNews < ActiveRecord::Base
211 self.table_name = :mantis_news_table
211 self.table_name = :mantis_news_table
212 end
212 end
213
213
214 class MantisCustomField < ActiveRecord::Base
214 class MantisCustomField < ActiveRecord::Base
215 self.table_name = :mantis_custom_field_table
215 self.table_name = :mantis_custom_field_table
216 set_inheritance_column :none
216 set_inheritance_column :none
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
217 has_many :values, :class_name => "MantisCustomFieldString", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
218 has_many :projects, :class_name => "MantisCustomFieldProject", :foreign_key => :field_id
219
219
220 def format
220 def format
221 read_attribute :type
221 read_attribute :type
222 end
222 end
223
223
224 def name
224 def name
225 read_attribute(:name)[0..29]
225 read_attribute(:name)[0..29]
226 end
226 end
227 end
227 end
228
228
229 class MantisCustomFieldProject < ActiveRecord::Base
229 class MantisCustomFieldProject < ActiveRecord::Base
230 self.table_name = :mantis_custom_field_project_table
230 self.table_name = :mantis_custom_field_project_table
231 end
231 end
232
232
233 class MantisCustomFieldString < ActiveRecord::Base
233 class MantisCustomFieldString < ActiveRecord::Base
234 self.table_name = :mantis_custom_field_string_table
234 self.table_name = :mantis_custom_field_string_table
235 end
235 end
236
236
237 def self.migrate
237 def self.migrate
238
238
239 # Users
239 # Users
240 print "Migrating users"
240 print "Migrating users"
241 User.delete_all "login <> 'admin'"
241 User.delete_all "login <> 'admin'"
242 users_map = {}
242 users_map = {}
243 users_migrated = 0
243 users_migrated = 0
244 MantisUser.find(:all).each do |user|
244 MantisUser.all.each do |user|
245 u = User.new :firstname => encode(user.firstname),
245 u = User.new :firstname => encode(user.firstname),
246 :lastname => encode(user.lastname),
246 :lastname => encode(user.lastname),
247 :mail => user.email,
247 :mail => user.email,
248 :last_login_on => user.last_visit
248 :last_login_on => user.last_visit
249 u.login = user.username
249 u.login = user.username
250 u.password = 'mantis'
250 u.password = 'mantis'
251 u.status = User::STATUS_LOCKED if user.enabled != 1
251 u.status = User::STATUS_LOCKED if user.enabled != 1
252 u.admin = true if user.access_level == 90
252 u.admin = true if user.access_level == 90
253 next unless u.save!
253 next unless u.save!
254 users_migrated += 1
254 users_migrated += 1
255 users_map[user.id] = u.id
255 users_map[user.id] = u.id
256 print '.'
256 print '.'
257 end
257 end
258 puts
258 puts
259
259
260 # Projects
260 # Projects
261 print "Migrating projects"
261 print "Migrating projects"
262 Project.destroy_all
262 Project.destroy_all
263 projects_map = {}
263 projects_map = {}
264 versions_map = {}
264 versions_map = {}
265 categories_map = {}
265 categories_map = {}
266 MantisProject.find(:all).each do |project|
266 MantisProject.all.each do |project|
267 p = Project.new :name => encode(project.name),
267 p = Project.new :name => encode(project.name),
268 :description => encode(project.description)
268 :description => encode(project.description)
269 p.identifier = project.identifier
269 p.identifier = project.identifier
270 next unless p.save
270 next unless p.save
271 projects_map[project.id] = p.id
271 projects_map[project.id] = p.id
272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
272 p.enabled_module_names = ['issue_tracking', 'news', 'wiki']
273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
273 p.trackers << TRACKER_BUG unless p.trackers.include?(TRACKER_BUG)
274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
274 p.trackers << TRACKER_FEATURE unless p.trackers.include?(TRACKER_FEATURE)
275 print '.'
275 print '.'
276
276
277 # Project members
277 # Project members
278 project.members.each do |member|
278 project.members.each do |member|
279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
279 m = Member.new :user => User.find_by_id(users_map[member.user_id]),
280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
280 :roles => [ROLE_MAPPING[member.access_level] || DEFAULT_ROLE]
281 m.project = p
281 m.project = p
282 m.save
282 m.save
283 end
283 end
284
284
285 # Project versions
285 # Project versions
286 project.versions.each do |version|
286 project.versions.each do |version|
287 v = Version.new :name => encode(version.version),
287 v = Version.new :name => encode(version.version),
288 :description => encode(version.description),
288 :description => encode(version.description),
289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
289 :effective_date => (version.date_order ? version.date_order.to_date : nil)
290 v.project = p
290 v.project = p
291 v.save
291 v.save
292 versions_map[version.id] = v.id
292 versions_map[version.id] = v.id
293 end
293 end
294
294
295 # Project categories
295 # Project categories
296 project.categories.each do |category|
296 project.categories.each do |category|
297 g = IssueCategory.new :name => category.category[0,30]
297 g = IssueCategory.new :name => category.category[0,30]
298 g.project = p
298 g.project = p
299 g.save
299 g.save
300 categories_map[category.category] = g.id
300 categories_map[category.category] = g.id
301 end
301 end
302 end
302 end
303 puts
303 puts
304
304
305 # Bugs
305 # Bugs
306 print "Migrating bugs"
306 print "Migrating bugs"
307 Issue.destroy_all
307 Issue.destroy_all
308 issues_map = {}
308 issues_map = {}
309 keep_bug_ids = (Issue.count == 0)
309 keep_bug_ids = (Issue.count == 0)
310 MantisBug.find_each(:batch_size => 200) do |bug|
310 MantisBug.find_each(:batch_size => 200) do |bug|
311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
311 next unless projects_map[bug.project_id] && users_map[bug.reporter_id]
312 i = Issue.new :project_id => projects_map[bug.project_id],
312 i = Issue.new :project_id => projects_map[bug.project_id],
313 :subject => encode(bug.summary),
313 :subject => encode(bug.summary),
314 :description => encode(bug.bug_text.full_description),
314 :description => encode(bug.bug_text.full_description),
315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
315 :priority => PRIORITY_MAPPING[bug.priority] || DEFAULT_PRIORITY,
316 :created_on => bug.date_submitted,
316 :created_on => bug.date_submitted,
317 :updated_on => bug.last_updated
317 :updated_on => bug.last_updated
318 i.author = User.find_by_id(users_map[bug.reporter_id])
318 i.author = User.find_by_id(users_map[bug.reporter_id])
319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
319 i.category = IssueCategory.find_by_project_id_and_name(i.project_id, bug.category[0,30]) unless bug.category.blank?
320 i.fixed_version = Version.find_by_project_id_and_name(i.project_id, bug.fixed_in_version) unless bug.fixed_in_version.blank?
320 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.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
321 i.status = STATUS_MAPPING[bug.status] || DEFAULT_STATUS
322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
322 i.tracker = (bug.severity == 10 ? TRACKER_FEATURE : TRACKER_BUG)
323 i.id = bug.id if keep_bug_ids
323 i.id = bug.id if keep_bug_ids
324 next unless i.save
324 next unless i.save
325 issues_map[bug.id] = i.id
325 issues_map[bug.id] = i.id
326 print '.'
326 print '.'
327 STDOUT.flush
327 STDOUT.flush
328
328
329 # Assignee
329 # Assignee
330 # Redmine checks that the assignee is a project member
330 # Redmine checks that the assignee is a project member
331 if (bug.handler_id && users_map[bug.handler_id])
331 if (bug.handler_id && users_map[bug.handler_id])
332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
332 i.assigned_to = User.find_by_id(users_map[bug.handler_id])
333 i.save(:validate => false)
333 i.save(:validate => false)
334 end
334 end
335
335
336 # Bug notes
336 # Bug notes
337 bug.bug_notes.each do |note|
337 bug.bug_notes.each do |note|
338 next unless users_map[note.reporter_id]
338 next unless users_map[note.reporter_id]
339 n = Journal.new :notes => encode(note.bug_note_text.note),
339 n = Journal.new :notes => encode(note.bug_note_text.note),
340 :created_on => note.date_submitted
340 :created_on => note.date_submitted
341 n.user = User.find_by_id(users_map[note.reporter_id])
341 n.user = User.find_by_id(users_map[note.reporter_id])
342 n.journalized = i
342 n.journalized = i
343 n.save
343 n.save
344 end
344 end
345
345
346 # Bug files
346 # Bug files
347 bug.bug_files.each do |file|
347 bug.bug_files.each do |file|
348 a = Attachment.new :created_on => file.date_added
348 a = Attachment.new :created_on => file.date_added
349 a.file = file
349 a.file = file
350 a.author = User.find :first
350 a.author = User.find :first
351 a.container = i
351 a.container = i
352 a.save
352 a.save
353 end
353 end
354
354
355 # Bug monitors
355 # Bug monitors
356 bug.bug_monitors.each do |monitor|
356 bug.bug_monitors.each do |monitor|
357 next unless users_map[monitor.user_id]
357 next unless users_map[monitor.user_id]
358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
358 i.add_watcher(User.find_by_id(users_map[monitor.user_id]))
359 end
359 end
360 end
360 end
361
361
362 # update issue id sequence if needed (postgresql)
362 # update issue id sequence if needed (postgresql)
363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
363 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
364 puts
364 puts
365
365
366 # Bug relationships
366 # Bug relationships
367 print "Migrating bug relations"
367 print "Migrating bug relations"
368 MantisBugRelationship.find(:all).each do |relation|
368 MantisBugRelationship.all.each do |relation|
369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
369 next unless issues_map[relation.source_bug_id] && issues_map[relation.destination_bug_id]
370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
370 r = IssueRelation.new :relation_type => RELATION_TYPE_MAPPING[relation.relationship_type]
371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
371 r.issue_from = Issue.find_by_id(issues_map[relation.source_bug_id])
372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
372 r.issue_to = Issue.find_by_id(issues_map[relation.destination_bug_id])
373 pp r unless r.save
373 pp r unless r.save
374 print '.'
374 print '.'
375 STDOUT.flush
375 STDOUT.flush
376 end
376 end
377 puts
377 puts
378
378
379 # News
379 # News
380 print "Migrating news"
380 print "Migrating news"
381 News.destroy_all
381 News.destroy_all
382 MantisNews.find(:all, :conditions => 'project_id > 0').each do |news|
382 MantisNews.where('project_id > 0').all.each do |news|
383 next unless projects_map[news.project_id]
383 next unless projects_map[news.project_id]
384 n = News.new :project_id => projects_map[news.project_id],
384 n = News.new :project_id => projects_map[news.project_id],
385 :title => encode(news.headline[0..59]),
385 :title => encode(news.headline[0..59]),
386 :description => encode(news.body),
386 :description => encode(news.body),
387 :created_on => news.date_posted
387 :created_on => news.date_posted
388 n.author = User.find_by_id(users_map[news.poster_id])
388 n.author = User.find_by_id(users_map[news.poster_id])
389 n.save
389 n.save
390 print '.'
390 print '.'
391 STDOUT.flush
391 STDOUT.flush
392 end
392 end
393 puts
393 puts
394
394
395 # Custom fields
395 # Custom fields
396 print "Migrating custom fields"
396 print "Migrating custom fields"
397 IssueCustomField.destroy_all
397 IssueCustomField.destroy_all
398 MantisCustomField.find(:all).each do |field|
398 MantisCustomField.all.each do |field|
399 f = IssueCustomField.new :name => field.name[0..29],
399 f = IssueCustomField.new :name => field.name[0..29],
400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
400 :field_format => CUSTOM_FIELD_TYPE_MAPPING[field.format],
401 :min_length => field.length_min,
401 :min_length => field.length_min,
402 :max_length => field.length_max,
402 :max_length => field.length_max,
403 :regexp => field.valid_regexp,
403 :regexp => field.valid_regexp,
404 :possible_values => field.possible_values.split('|'),
404 :possible_values => field.possible_values.split('|'),
405 :is_required => field.require_report?
405 :is_required => field.require_report?
406 next unless f.save
406 next unless f.save
407 print '.'
407 print '.'
408 STDOUT.flush
408 STDOUT.flush
409 # Trackers association
409 # Trackers association
410 f.trackers = Tracker.find :all
410 f.trackers = Tracker.all
411
411
412 # Projects association
412 # Projects association
413 field.projects.each do |project|
413 field.projects.each do |project|
414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
414 f.projects << Project.find_by_id(projects_map[project.project_id]) if projects_map[project.project_id]
415 end
415 end
416
416
417 # Values
417 # Values
418 field.values.each do |value|
418 field.values.each do |value|
419 v = CustomValue.new :custom_field_id => f.id,
419 v = CustomValue.new :custom_field_id => f.id,
420 :value => value.value
420 :value => value.value
421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
421 v.customized = Issue.find_by_id(issues_map[value.bug_id]) if issues_map[value.bug_id]
422 v.save
422 v.save
423 end unless f.new_record?
423 end unless f.new_record?
424 end
424 end
425 puts
425 puts
426
426
427 puts
427 puts
428 puts "Users: #{users_migrated}/#{MantisUser.count}"
428 puts "Users: #{users_migrated}/#{MantisUser.count}"
429 puts "Projects: #{Project.count}/#{MantisProject.count}"
429 puts "Projects: #{Project.count}/#{MantisProject.count}"
430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
430 puts "Memberships: #{Member.count}/#{MantisProjectUser.count}"
431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
431 puts "Versions: #{Version.count}/#{MantisVersion.count}"
432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
432 puts "Categories: #{IssueCategory.count}/#{MantisCategory.count}"
433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
433 puts "Bugs: #{Issue.count}/#{MantisBug.count}"
434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
434 puts "Bug notes: #{Journal.count}/#{MantisBugNote.count}"
435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
435 puts "Bug files: #{Attachment.count}/#{MantisBugFile.count}"
436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
436 puts "Bug relations: #{IssueRelation.count}/#{MantisBugRelationship.count}"
437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
437 puts "Bug monitors: #{Watcher.count}/#{MantisBugMonitor.count}"
438 puts "News: #{News.count}/#{MantisNews.count}"
438 puts "News: #{News.count}/#{MantisNews.count}"
439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
439 puts "Custom fields: #{IssueCustomField.count}/#{MantisCustomField.count}"
440 end
440 end
441
441
442 def self.encoding(charset)
442 def self.encoding(charset)
443 @ic = Iconv.new('UTF-8', charset)
443 @ic = Iconv.new('UTF-8', charset)
444 rescue Iconv::InvalidEncoding
444 rescue Iconv::InvalidEncoding
445 return false
445 return false
446 end
446 end
447
447
448 def self.establish_connection(params)
448 def self.establish_connection(params)
449 constants.each do |const|
449 constants.each do |const|
450 klass = const_get(const)
450 klass = const_get(const)
451 next unless klass.respond_to? 'establish_connection'
451 next unless klass.respond_to? 'establish_connection'
452 klass.establish_connection params
452 klass.establish_connection params
453 end
453 end
454 end
454 end
455
455
456 def self.encode(text)
456 def self.encode(text)
457 @ic.iconv text
457 @ic.iconv text
458 rescue
458 rescue
459 text
459 text
460 end
460 end
461 end
461 end
462
462
463 puts
463 puts
464 if Redmine::DefaultData::Loader.no_data?
464 if Redmine::DefaultData::Loader.no_data?
465 puts "Redmine configuration need to be loaded before importing data."
465 puts "Redmine configuration need to be loaded before importing data."
466 puts "Please, run this first:"
466 puts "Please, run this first:"
467 puts
467 puts
468 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
468 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
469 exit
469 exit
470 end
470 end
471
471
472 puts "WARNING: Your Redmine data will be deleted during this process."
472 puts "WARNING: Your Redmine data will be deleted during this process."
473 print "Are you sure you want to continue ? [y/N] "
473 print "Are you sure you want to continue ? [y/N] "
474 STDOUT.flush
474 STDOUT.flush
475 break unless STDIN.gets.match(/^y$/i)
475 break unless STDIN.gets.match(/^y$/i)
476
476
477 # Default Mantis database settings
477 # Default Mantis database settings
478 db_params = {:adapter => 'mysql2',
478 db_params = {:adapter => 'mysql2',
479 :database => 'bugtracker',
479 :database => 'bugtracker',
480 :host => 'localhost',
480 :host => 'localhost',
481 :username => 'root',
481 :username => 'root',
482 :password => '' }
482 :password => '' }
483
483
484 puts
484 puts
485 puts "Please enter settings for your Mantis database"
485 puts "Please enter settings for your Mantis database"
486 [:adapter, :host, :database, :username, :password].each do |param|
486 [:adapter, :host, :database, :username, :password].each do |param|
487 print "#{param} [#{db_params[param]}]: "
487 print "#{param} [#{db_params[param]}]: "
488 value = STDIN.gets.chomp!
488 value = STDIN.gets.chomp!
489 db_params[param] = value unless value.blank?
489 db_params[param] = value unless value.blank?
490 end
490 end
491
491
492 while true
492 while true
493 print "encoding [UTF-8]: "
493 print "encoding [UTF-8]: "
494 STDOUT.flush
494 STDOUT.flush
495 encoding = STDIN.gets.chomp!
495 encoding = STDIN.gets.chomp!
496 encoding = 'UTF-8' if encoding.blank?
496 encoding = 'UTF-8' if encoding.blank?
497 break if MantisMigrate.encoding encoding
497 break if MantisMigrate.encoding encoding
498 puts "Invalid encoding!"
498 puts "Invalid encoding!"
499 end
499 end
500 puts
500 puts
501
501
502 # Make sure bugs can refer bugs in other projects
502 # Make sure bugs can refer bugs in other projects
503 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
503 Setting.cross_project_issue_relations = 1 if Setting.respond_to? 'cross_project_issue_relations'
504
504
505 # Turn off email notifications
505 # Turn off email notifications
506 Setting.notified_events = []
506 Setting.notified_events = []
507
507
508 MantisMigrate.establish_connection db_params
508 MantisMigrate.establish_connection db_params
509 MantisMigrate.migrate
509 MantisMigrate.migrate
510 end
510 end
511 end
511 end
@@ -1,772 +1,772
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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.where(:builtin => 0).order('position ASC').all
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 self.table_name = :component
88 self.table_name = :component
89 end
89 end
90
90
91 class TracMilestone < ActiveRecord::Base
91 class TracMilestone < ActiveRecord::Base
92 self.table_name = :milestone
92 self.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 self.table_name = :ticket_custom
117 self.table_name = :ticket_custom
118 end
118 end
119
119
120 class TracAttachment < ActiveRecord::Base
120 class TracAttachment < ActiveRecord::Base
121 self.table_name = :attachment
121 self.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 self.table_name = :ticket
162 self.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 :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
166 has_many :ticket_changes, :class_name => "TracTicketChange", :foreign_key => :ticket
167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
167 has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
168
168
169 def attachments
169 def attachments
170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
170 TracMigrate::TracAttachment.all(:conditions => ["type = 'ticket' AND id = ?", self.id.to_s])
171 end
171 end
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 self.table_name = :ticket_change
190 self.table_name = :ticket_change
191
191
192 def self.columns
192 def self.columns
193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
193 # Hides Trac field 'field' to prevent clash with AR field_changed? method (Rails 3.0)
194 super.select {|column| column.name.to_s != 'field'}
194 super.select {|column| column.name.to_s != 'field'}
195 end
195 end
196
196
197 def time; Time.at(read_attribute(:time)) end
197 def time; Time.at(read_attribute(:time)) end
198 end
198 end
199
199
200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
200 TRAC_WIKI_PAGES = %w(InterMapTxt InterTrac InterWiki RecentChanges SandBox TracAccessibility TracAdmin TracBackup TracBrowser TracCgi TracChangeset \
201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
201 TracEnvironment TracFastCgi TracGuide TracImport TracIni TracInstall TracInterfaceCustomization \
202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
202 TracLinks TracLogging TracModPython TracNotification TracPermissions TracPlugins TracQuery \
203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
203 TracReports TracRevisionLog TracRoadmap TracRss TracSearch TracStandalone TracSupport TracSyntaxColoring TracTickets \
204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
204 TracTicketsCustomFields TracTimeline TracUnicode TracUpgrade TracWiki WikiDeletePage WikiFormatting \
205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
205 WikiHtml WikiMacros WikiNewPage WikiPageNames WikiProcessors WikiRestructuredText WikiRestructuredTextLinks \
206 CamelCase TitleIndex)
206 CamelCase TitleIndex)
207
207
208 class TracWikiPage < ActiveRecord::Base
208 class TracWikiPage < ActiveRecord::Base
209 self.table_name = :wiki
209 self.table_name = :wiki
210 set_primary_key :name
210 set_primary_key :name
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 attachments
217 def attachments
218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
218 TracMigrate::TracAttachment.all(:conditions => ["type = 'wiki' AND id = ?", self.id.to_s])
219 end
219 end
220
220
221 def time; Time.at(read_attribute(:time)) end
221 def time; Time.at(read_attribute(:time)) end
222 end
222 end
223
223
224 class TracPermission < ActiveRecord::Base
224 class TracPermission < ActiveRecord::Base
225 self.table_name = :permission
225 self.table_name = :permission
226 end
226 end
227
227
228 class TracSessionAttribute < ActiveRecord::Base
228 class TracSessionAttribute < ActiveRecord::Base
229 self.table_name = :session_attribute
229 self.table_name = :session_attribute
230 end
230 end
231
231
232 def self.find_or_create_user(username, project_member = false)
232 def self.find_or_create_user(username, project_member = false)
233 return User.anonymous if username.blank?
233 return User.anonymous if username.blank?
234
234
235 u = User.find_by_login(username)
235 u = User.find_by_login(username)
236 if !u
236 if !u
237 # Create a new user if not found
237 # Create a new user if not found
238 mail = username[0, User::MAIL_LENGTH_LIMIT]
238 mail = username[0, User::MAIL_LENGTH_LIMIT]
239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
239 if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
240 mail = mail_attr.value
240 mail = mail_attr.value
241 end
241 end
242 mail = "#{mail}@foo.bar" unless mail.include?("@")
242 mail = "#{mail}@foo.bar" unless mail.include?("@")
243
243
244 name = username
244 name = username
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
245 if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
246 name = name_attr.value
246 name = name_attr.value
247 end
247 end
248 name =~ (/(.*)(\s+\w+)?/)
248 name =~ (/(.*)(\s+\w+)?/)
249 fn = $1.strip
249 fn = $1.strip
250 ln = ($2 || '-').strip
250 ln = ($2 || '-').strip
251
251
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
252 u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
253 :firstname => fn[0, limit_for(User, 'firstname')],
253 :firstname => fn[0, limit_for(User, 'firstname')],
254 :lastname => ln[0, limit_for(User, 'lastname')]
254 :lastname => ln[0, limit_for(User, 'lastname')]
255
255
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
256 u.login = username[0, User::LOGIN_LENGTH_LIMIT].gsub(/[^a-z0-9_\-@\.]/i, '-')
257 u.password = 'trac'
257 u.password = 'trac'
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
258 u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
259 # finally, a default user is used if the new user is not valid
259 # finally, a default user is used if the new user is not valid
260 u = User.find(:first) unless u.save
260 u = User.find(:first) unless u.save
261 end
261 end
262 # Make sure he is a member of the project
262 # Make sure he is a member of the project
263 if project_member && !u.member_of?(@target_project)
263 if project_member && !u.member_of?(@target_project)
264 role = DEFAULT_ROLE
264 role = DEFAULT_ROLE
265 if u.admin
265 if u.admin
266 role = ROLE_MAPPING['admin']
266 role = ROLE_MAPPING['admin']
267 elsif TracPermission.find_by_username_and_action(username, 'developer')
267 elsif TracPermission.find_by_username_and_action(username, 'developer')
268 role = ROLE_MAPPING['developer']
268 role = ROLE_MAPPING['developer']
269 end
269 end
270 Member.create(:user => u, :project => @target_project, :roles => [role])
270 Member.create(:user => u, :project => @target_project, :roles => [role])
271 u.reload
271 u.reload
272 end
272 end
273 u
273 u
274 end
274 end
275
275
276 # Basic wiki syntax conversion
276 # Basic wiki syntax conversion
277 def self.convert_wiki_text(text)
277 def self.convert_wiki_text(text)
278 # Titles
278 # Titles
279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
279 text = text.gsub(/^(\=+)\s(.+)\s(\=+)/) {|s| "\nh#{$1.length}. #{$2}\n"}
280 # External Links
280 # External Links
281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
281 text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
282 # Ticket links:
282 # Ticket links:
283 # [ticket:234 Text],[ticket:234 This is a test]
283 # [ticket:234 Text],[ticket:234 This is a test]
284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
284 text = text.gsub(/\[ticket\:([^\ ]+)\ (.+?)\]/, '"\2":/issues/show/\1')
285 # ticket:1234
285 # ticket:1234
286 # #1 is working cause Redmine uses the same syntax.
286 # #1 is working cause Redmine uses the same syntax.
287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
287 text = text.gsub(/ticket\:([^\ ]+)/, '#\1')
288 # Milestone links:
288 # Milestone links:
289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
289 # [milestone:"0.1.0 Mercury" Milestone 0.1.0 (Mercury)]
290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
290 # The text "Milestone 0.1.0 (Mercury)" is not converted,
291 # cause Redmine's wiki does not support this.
291 # cause Redmine's wiki does not support this.
292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
292 text = text.gsub(/\[milestone\:\"([^\"]+)\"\ (.+?)\]/, 'version:"\1"')
293 # [milestone:"0.1.0 Mercury"]
293 # [milestone:"0.1.0 Mercury"]
294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
294 text = text.gsub(/\[milestone\:\"([^\"]+)\"\]/, 'version:"\1"')
295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
295 text = text.gsub(/milestone\:\"([^\"]+)\"/, 'version:"\1"')
296 # milestone:0.1.0
296 # milestone:0.1.0
297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
297 text = text.gsub(/\[milestone\:([^\ ]+)\]/, 'version:\1')
298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
298 text = text.gsub(/milestone\:([^\ ]+)/, 'version:\1')
299 # Internal Links
299 # Internal Links
300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
300 text = text.gsub(/\[\[BR\]\]/, "\n") # This has to go before the rules below
301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
301 text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
302 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
303 text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
304 text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
305 text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
306
306
307 # Links to pages UsingJustWikiCaps
307 # Links to pages UsingJustWikiCaps
308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
308 text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
309 # Normalize things that were supposed to not be links
309 # Normalize things that were supposed to not be links
310 # like !NotALink
310 # like !NotALink
311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
311 text = text.gsub(/(^| )!([A-Z][A-Za-z]+)/, '\1\2')
312 # Revisions links
312 # Revisions links
313 text = text.gsub(/\[(\d+)\]/, 'r\1')
313 text = text.gsub(/\[(\d+)\]/, 'r\1')
314 # Ticket number re-writing
314 # Ticket number re-writing
315 text = text.gsub(/#(\d+)/) do |s|
315 text = text.gsub(/#(\d+)/) do |s|
316 if $1.length < 10
316 if $1.length < 10
317 # TICKET_MAP[$1.to_i] ||= $1
317 # TICKET_MAP[$1.to_i] ||= $1
318 "\##{TICKET_MAP[$1.to_i] || $1}"
318 "\##{TICKET_MAP[$1.to_i] || $1}"
319 else
319 else
320 s
320 s
321 end
321 end
322 end
322 end
323 # We would like to convert the Code highlighting too
323 # We would like to convert the Code highlighting too
324 # This will go into the next line.
324 # This will go into the next line.
325 shebang_line = false
325 shebang_line = false
326 # Reguar expression for start of code
326 # Reguar expression for start of code
327 pre_re = /\{\{\{/
327 pre_re = /\{\{\{/
328 # Code hightlighing...
328 # Code hightlighing...
329 shebang_re = /^\#\!([a-z]+)/
329 shebang_re = /^\#\!([a-z]+)/
330 # Regular expression for end of code
330 # Regular expression for end of code
331 pre_end_re = /\}\}\}/
331 pre_end_re = /\}\}\}/
332
332
333 # Go through the whole text..extract it line by line
333 # Go through the whole text..extract it line by line
334 text = text.gsub(/^(.*)$/) do |line|
334 text = text.gsub(/^(.*)$/) do |line|
335 m_pre = pre_re.match(line)
335 m_pre = pre_re.match(line)
336 if m_pre
336 if m_pre
337 line = '<pre>'
337 line = '<pre>'
338 else
338 else
339 m_sl = shebang_re.match(line)
339 m_sl = shebang_re.match(line)
340 if m_sl
340 if m_sl
341 shebang_line = true
341 shebang_line = true
342 line = '<code class="' + m_sl[1] + '">'
342 line = '<code class="' + m_sl[1] + '">'
343 end
343 end
344 m_pre_end = pre_end_re.match(line)
344 m_pre_end = pre_end_re.match(line)
345 if m_pre_end
345 if m_pre_end
346 line = '</pre>'
346 line = '</pre>'
347 if shebang_line
347 if shebang_line
348 line = '</code>' + line
348 line = '</code>' + line
349 end
349 end
350 end
350 end
351 end
351 end
352 line
352 line
353 end
353 end
354
354
355 # Highlighting
355 # Highlighting
356 text = text.gsub(/'''''([^\s])/, '_*\1')
356 text = text.gsub(/'''''([^\s])/, '_*\1')
357 text = text.gsub(/([^\s])'''''/, '\1*_')
357 text = text.gsub(/([^\s])'''''/, '\1*_')
358 text = text.gsub(/'''/, '*')
358 text = text.gsub(/'''/, '*')
359 text = text.gsub(/''/, '_')
359 text = text.gsub(/''/, '_')
360 text = text.gsub(/__/, '+')
360 text = text.gsub(/__/, '+')
361 text = text.gsub(/~~/, '-')
361 text = text.gsub(/~~/, '-')
362 text = text.gsub(/`/, '@')
362 text = text.gsub(/`/, '@')
363 text = text.gsub(/,,/, '~')
363 text = text.gsub(/,,/, '~')
364 # Lists
364 # Lists
365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
365 text = text.gsub(/^([ ]+)\* /) {|s| '*' * $1.length + " "}
366
366
367 text
367 text
368 end
368 end
369
369
370 def self.migrate
370 def self.migrate
371 establish_connection
371 establish_connection
372
372
373 # Quick database test
373 # Quick database test
374 TracComponent.count
374 TracComponent.count
375
375
376 migrated_components = 0
376 migrated_components = 0
377 migrated_milestones = 0
377 migrated_milestones = 0
378 migrated_tickets = 0
378 migrated_tickets = 0
379 migrated_custom_values = 0
379 migrated_custom_values = 0
380 migrated_ticket_attachments = 0
380 migrated_ticket_attachments = 0
381 migrated_wiki_edits = 0
381 migrated_wiki_edits = 0
382 migrated_wiki_attachments = 0
382 migrated_wiki_attachments = 0
383
383
384 #Wiki system initializing...
384 #Wiki system initializing...
385 @target_project.wiki.destroy if @target_project.wiki
385 @target_project.wiki.destroy if @target_project.wiki
386 @target_project.reload
386 @target_project.reload
387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
387 wiki = Wiki.new(:project => @target_project, :start_page => 'WikiStart')
388 wiki_edit_count = 0
388 wiki_edit_count = 0
389
389
390 # Components
390 # Components
391 print "Migrating components"
391 print "Migrating components"
392 issues_category_map = {}
392 issues_category_map = {}
393 TracComponent.find(:all).each do |component|
393 TracComponent.all.each do |component|
394 print '.'
394 print '.'
395 STDOUT.flush
395 STDOUT.flush
396 c = IssueCategory.new :project => @target_project,
396 c = IssueCategory.new :project => @target_project,
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
397 :name => encode(component.name[0, limit_for(IssueCategory, 'name')])
398 next unless c.save
398 next unless c.save
399 issues_category_map[component.name] = c
399 issues_category_map[component.name] = c
400 migrated_components += 1
400 migrated_components += 1
401 end
401 end
402 puts
402 puts
403
403
404 # Milestones
404 # Milestones
405 print "Migrating milestones"
405 print "Migrating milestones"
406 version_map = {}
406 version_map = {}
407 TracMilestone.find(:all).each do |milestone|
407 TracMilestone.all.each do |milestone|
408 print '.'
408 print '.'
409 STDOUT.flush
409 STDOUT.flush
410 # First we try to find the wiki page...
410 # First we try to find the wiki page...
411 p = wiki.find_or_new_page(milestone.name.to_s)
411 p = wiki.find_or_new_page(milestone.name.to_s)
412 p.content = WikiContent.new(:page => p) if p.new_record?
412 p.content = WikiContent.new(:page => p) if p.new_record?
413 p.content.text = milestone.description.to_s
413 p.content.text = milestone.description.to_s
414 p.content.author = find_or_create_user('trac')
414 p.content.author = find_or_create_user('trac')
415 p.content.comments = 'Milestone'
415 p.content.comments = 'Milestone'
416 p.save
416 p.save
417
417
418 v = Version.new :project => @target_project,
418 v = Version.new :project => @target_project,
419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
419 :name => encode(milestone.name[0, limit_for(Version, 'name')]),
420 :description => nil,
420 :description => nil,
421 :wiki_page_title => milestone.name.to_s,
421 :wiki_page_title => milestone.name.to_s,
422 :effective_date => milestone.completed
422 :effective_date => milestone.completed
423
423
424 next unless v.save
424 next unless v.save
425 version_map[milestone.name] = v
425 version_map[milestone.name] = v
426 migrated_milestones += 1
426 migrated_milestones += 1
427 end
427 end
428 puts
428 puts
429
429
430 # Custom fields
430 # Custom fields
431 # TODO: read trac.ini instead
431 # TODO: read trac.ini instead
432 print "Migrating custom fields"
432 print "Migrating custom fields"
433 custom_field_map = {}
433 custom_field_map = {}
434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
434 TracTicketCustom.find_by_sql("SELECT DISTINCT name FROM #{TracTicketCustom.table_name}").each do |field|
435 print '.'
435 print '.'
436 STDOUT.flush
436 STDOUT.flush
437 # Redmine custom field name
437 # Redmine custom field name
438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
438 field_name = encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize
439 # Find if the custom already exists in Redmine
439 # Find if the custom already exists in Redmine
440 f = IssueCustomField.find_by_name(field_name)
440 f = IssueCustomField.find_by_name(field_name)
441 # Or create a new one
441 # Or create a new one
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
442 f ||= IssueCustomField.create(:name => encode(field.name[0, limit_for(IssueCustomField, 'name')]).humanize,
443 :field_format => 'string')
443 :field_format => 'string')
444
444
445 next if f.new_record?
445 next if f.new_record?
446 f.trackers = Tracker.find(:all)
446 f.trackers = Tracker.all
447 f.projects << @target_project
447 f.projects << @target_project
448 custom_field_map[field.name] = f
448 custom_field_map[field.name] = f
449 end
449 end
450 puts
450 puts
451
451
452 # Trac 'resolution' field as a Redmine custom field
452 # Trac 'resolution' field as a Redmine custom field
453 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
453 r = IssueCustomField.find(:first, :conditions => { :name => "Resolution" })
454 r = IssueCustomField.new(:name => 'Resolution',
454 r = IssueCustomField.new(:name => 'Resolution',
455 :field_format => 'list',
455 :field_format => 'list',
456 :is_filter => true) if r.nil?
456 :is_filter => true) if r.nil?
457 r.trackers = Tracker.find(:all)
457 r.trackers = Tracker.all
458 r.projects << @target_project
458 r.projects << @target_project
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
459 r.possible_values = (r.possible_values + %w(fixed invalid wontfix duplicate worksforme)).flatten.compact.uniq
460 r.save!
460 r.save!
461 custom_field_map['resolution'] = r
461 custom_field_map['resolution'] = r
462
462
463 # Tickets
463 # Tickets
464 print "Migrating tickets"
464 print "Migrating tickets"
465 TracTicket.find_each(:batch_size => 200) do |ticket|
465 TracTicket.find_each(:batch_size => 200) do |ticket|
466 print '.'
466 print '.'
467 STDOUT.flush
467 STDOUT.flush
468 i = Issue.new :project => @target_project,
468 i = Issue.new :project => @target_project,
469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
469 :subject => encode(ticket.summary[0, limit_for(Issue, 'subject')]),
470 :description => convert_wiki_text(encode(ticket.description)),
470 :description => convert_wiki_text(encode(ticket.description)),
471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
471 :priority => PRIORITY_MAPPING[ticket.priority] || DEFAULT_PRIORITY,
472 :created_on => ticket.time
472 :created_on => ticket.time
473 i.author = find_or_create_user(ticket.reporter)
473 i.author = find_or_create_user(ticket.reporter)
474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
474 i.category = issues_category_map[ticket.component] unless ticket.component.blank?
475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
475 i.fixed_version = version_map[ticket.milestone] unless ticket.milestone.blank?
476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
476 i.status = STATUS_MAPPING[ticket.status] || DEFAULT_STATUS
477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
477 i.tracker = TRACKER_MAPPING[ticket.ticket_type] || DEFAULT_TRACKER
478 i.id = ticket.id unless Issue.exists?(ticket.id)
478 i.id = ticket.id unless Issue.exists?(ticket.id)
479 next unless Time.fake(ticket.changetime) { i.save }
479 next unless Time.fake(ticket.changetime) { i.save }
480 TICKET_MAP[ticket.id] = i.id
480 TICKET_MAP[ticket.id] = i.id
481 migrated_tickets += 1
481 migrated_tickets += 1
482
482
483 # Owner
483 # Owner
484 unless ticket.owner.blank?
484 unless ticket.owner.blank?
485 i.assigned_to = find_or_create_user(ticket.owner, true)
485 i.assigned_to = find_or_create_user(ticket.owner, true)
486 Time.fake(ticket.changetime) { i.save }
486 Time.fake(ticket.changetime) { i.save }
487 end
487 end
488
488
489 # Comments and status/resolution changes
489 # Comments and status/resolution changes
490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
490 ticket.ticket_changes.group_by(&:time).each do |time, changeset|
491 status_change = changeset.select {|change| change.field == 'status'}.first
491 status_change = changeset.select {|change| change.field == 'status'}.first
492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
492 resolution_change = changeset.select {|change| change.field == 'resolution'}.first
493 comment_change = changeset.select {|change| change.field == 'comment'}.first
493 comment_change = changeset.select {|change| change.field == 'comment'}.first
494
494
495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
495 n = Journal.new :notes => (comment_change ? convert_wiki_text(encode(comment_change.newvalue)) : ''),
496 :created_on => time
496 :created_on => time
497 n.user = find_or_create_user(changeset.first.author)
497 n.user = find_or_create_user(changeset.first.author)
498 n.journalized = i
498 n.journalized = i
499 if status_change &&
499 if status_change &&
500 STATUS_MAPPING[status_change.oldvalue] &&
500 STATUS_MAPPING[status_change.oldvalue] &&
501 STATUS_MAPPING[status_change.newvalue] &&
501 STATUS_MAPPING[status_change.newvalue] &&
502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
502 (STATUS_MAPPING[status_change.oldvalue] != STATUS_MAPPING[status_change.newvalue])
503 n.details << JournalDetail.new(:property => 'attr',
503 n.details << JournalDetail.new(:property => 'attr',
504 :prop_key => 'status_id',
504 :prop_key => 'status_id',
505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
505 :old_value => STATUS_MAPPING[status_change.oldvalue].id,
506 :value => STATUS_MAPPING[status_change.newvalue].id)
506 :value => STATUS_MAPPING[status_change.newvalue].id)
507 end
507 end
508 if resolution_change
508 if resolution_change
509 n.details << JournalDetail.new(:property => 'cf',
509 n.details << JournalDetail.new(:property => 'cf',
510 :prop_key => custom_field_map['resolution'].id,
510 :prop_key => custom_field_map['resolution'].id,
511 :old_value => resolution_change.oldvalue,
511 :old_value => resolution_change.oldvalue,
512 :value => resolution_change.newvalue)
512 :value => resolution_change.newvalue)
513 end
513 end
514 n.save unless n.details.empty? && n.notes.blank?
514 n.save unless n.details.empty? && n.notes.blank?
515 end
515 end
516
516
517 # Attachments
517 # Attachments
518 ticket.attachments.each do |attachment|
518 ticket.attachments.each do |attachment|
519 next unless attachment.exist?
519 next unless attachment.exist?
520 attachment.open {
520 attachment.open {
521 a = Attachment.new :created_on => attachment.time
521 a = Attachment.new :created_on => attachment.time
522 a.file = attachment
522 a.file = attachment
523 a.author = find_or_create_user(attachment.author)
523 a.author = find_or_create_user(attachment.author)
524 a.container = i
524 a.container = i
525 a.description = attachment.description
525 a.description = attachment.description
526 migrated_ticket_attachments += 1 if a.save
526 migrated_ticket_attachments += 1 if a.save
527 }
527 }
528 end
528 end
529
529
530 # Custom fields
530 # Custom fields
531 custom_values = ticket.customs.inject({}) do |h, custom|
531 custom_values = ticket.customs.inject({}) do |h, custom|
532 if custom_field = custom_field_map[custom.name]
532 if custom_field = custom_field_map[custom.name]
533 h[custom_field.id] = custom.value
533 h[custom_field.id] = custom.value
534 migrated_custom_values += 1
534 migrated_custom_values += 1
535 end
535 end
536 h
536 h
537 end
537 end
538 if custom_field_map['resolution'] && !ticket.resolution.blank?
538 if custom_field_map['resolution'] && !ticket.resolution.blank?
539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
539 custom_values[custom_field_map['resolution'].id] = ticket.resolution
540 end
540 end
541 i.custom_field_values = custom_values
541 i.custom_field_values = custom_values
542 i.save_custom_field_values
542 i.save_custom_field_values
543 end
543 end
544
544
545 # update issue id sequence if needed (postgresql)
545 # update issue id sequence if needed (postgresql)
546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
546 Issue.connection.reset_pk_sequence!(Issue.table_name) if Issue.connection.respond_to?('reset_pk_sequence!')
547 puts
547 puts
548
548
549 # Wiki
549 # Wiki
550 print "Migrating wiki"
550 print "Migrating wiki"
551 if wiki.save
551 if wiki.save
552 TracWikiPage.find(:all, :order => 'name, version').each do |page|
552 TracWikiPage.order('name, version').all.each do |page|
553 # Do not migrate Trac manual wiki pages
553 # Do not migrate Trac manual wiki pages
554 next if TRAC_WIKI_PAGES.include?(page.name)
554 next if TRAC_WIKI_PAGES.include?(page.name)
555 wiki_edit_count += 1
555 wiki_edit_count += 1
556 print '.'
556 print '.'
557 STDOUT.flush
557 STDOUT.flush
558 p = wiki.find_or_new_page(page.name)
558 p = wiki.find_or_new_page(page.name)
559 p.content = WikiContent.new(:page => p) if p.new_record?
559 p.content = WikiContent.new(:page => p) if p.new_record?
560 p.content.text = page.text
560 p.content.text = page.text
561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
561 p.content.author = find_or_create_user(page.author) unless page.author.blank? || page.author == 'trac'
562 p.content.comments = page.comment
562 p.content.comments = page.comment
563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
563 Time.fake(page.time) { p.new_record? ? p.save : p.content.save }
564
564
565 next if p.content.new_record?
565 next if p.content.new_record?
566 migrated_wiki_edits += 1
566 migrated_wiki_edits += 1
567
567
568 # Attachments
568 # Attachments
569 page.attachments.each do |attachment|
569 page.attachments.each do |attachment|
570 next unless attachment.exist?
570 next unless attachment.exist?
571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
571 next if p.attachments.find_by_filename(attachment.filename.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/,'_')) #add only once per page
572 attachment.open {
572 attachment.open {
573 a = Attachment.new :created_on => attachment.time
573 a = Attachment.new :created_on => attachment.time
574 a.file = attachment
574 a.file = attachment
575 a.author = find_or_create_user(attachment.author)
575 a.author = find_or_create_user(attachment.author)
576 a.description = attachment.description
576 a.description = attachment.description
577 a.container = p
577 a.container = p
578 migrated_wiki_attachments += 1 if a.save
578 migrated_wiki_attachments += 1 if a.save
579 }
579 }
580 end
580 end
581 end
581 end
582
582
583 wiki.reload
583 wiki.reload
584 wiki.pages.each do |page|
584 wiki.pages.each do |page|
585 page.content.text = convert_wiki_text(page.content.text)
585 page.content.text = convert_wiki_text(page.content.text)
586 Time.fake(page.content.updated_on) { page.content.save }
586 Time.fake(page.content.updated_on) { page.content.save }
587 end
587 end
588 end
588 end
589 puts
589 puts
590
590
591 puts
591 puts
592 puts "Components: #{migrated_components}/#{TracComponent.count}"
592 puts "Components: #{migrated_components}/#{TracComponent.count}"
593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
593 puts "Milestones: #{migrated_milestones}/#{TracMilestone.count}"
594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
594 puts "Tickets: #{migrated_tickets}/#{TracTicket.count}"
595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
595 puts "Ticket files: #{migrated_ticket_attachments}/" + TracAttachment.count(:conditions => {:type => 'ticket'}).to_s
596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
596 puts "Custom values: #{migrated_custom_values}/#{TracTicketCustom.count}"
597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
597 puts "Wiki edits: #{migrated_wiki_edits}/#{wiki_edit_count}"
598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
598 puts "Wiki files: #{migrated_wiki_attachments}/" + TracAttachment.count(:conditions => {:type => 'wiki'}).to_s
599 end
599 end
600
600
601 def self.limit_for(klass, attribute)
601 def self.limit_for(klass, attribute)
602 klass.columns_hash[attribute.to_s].limit
602 klass.columns_hash[attribute.to_s].limit
603 end
603 end
604
604
605 def self.encoding(charset)
605 def self.encoding(charset)
606 @ic = Iconv.new('UTF-8', charset)
606 @ic = Iconv.new('UTF-8', charset)
607 rescue Iconv::InvalidEncoding
607 rescue Iconv::InvalidEncoding
608 puts "Invalid encoding!"
608 puts "Invalid encoding!"
609 return false
609 return false
610 end
610 end
611
611
612 def self.set_trac_directory(path)
612 def self.set_trac_directory(path)
613 @@trac_directory = path
613 @@trac_directory = path
614 raise "This directory doesn't exist!" unless File.directory?(path)
614 raise "This directory doesn't exist!" unless File.directory?(path)
615 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
615 raise "#{trac_attachments_directory} doesn't exist!" unless File.directory?(trac_attachments_directory)
616 @@trac_directory
616 @@trac_directory
617 rescue Exception => e
617 rescue Exception => e
618 puts e
618 puts e
619 return false
619 return false
620 end
620 end
621
621
622 def self.trac_directory
622 def self.trac_directory
623 @@trac_directory
623 @@trac_directory
624 end
624 end
625
625
626 def self.set_trac_adapter(adapter)
626 def self.set_trac_adapter(adapter)
627 return false if adapter.blank?
627 return false if adapter.blank?
628 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
628 raise "Unknown adapter: #{adapter}!" unless %w(sqlite3 mysql postgresql).include?(adapter)
629 # If adapter is sqlite or sqlite3, make sure that trac.db exists
629 # If adapter is sqlite or sqlite3, make sure that trac.db exists
630 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
630 raise "#{trac_db_path} doesn't exist!" if %w(sqlite3).include?(adapter) && !File.exist?(trac_db_path)
631 @@trac_adapter = adapter
631 @@trac_adapter = adapter
632 rescue Exception => e
632 rescue Exception => e
633 puts e
633 puts e
634 return false
634 return false
635 end
635 end
636
636
637 def self.set_trac_db_host(host)
637 def self.set_trac_db_host(host)
638 return nil if host.blank?
638 return nil if host.blank?
639 @@trac_db_host = host
639 @@trac_db_host = host
640 end
640 end
641
641
642 def self.set_trac_db_port(port)
642 def self.set_trac_db_port(port)
643 return nil if port.to_i == 0
643 return nil if port.to_i == 0
644 @@trac_db_port = port.to_i
644 @@trac_db_port = port.to_i
645 end
645 end
646
646
647 def self.set_trac_db_name(name)
647 def self.set_trac_db_name(name)
648 return nil if name.blank?
648 return nil if name.blank?
649 @@trac_db_name = name
649 @@trac_db_name = name
650 end
650 end
651
651
652 def self.set_trac_db_username(username)
652 def self.set_trac_db_username(username)
653 @@trac_db_username = username
653 @@trac_db_username = username
654 end
654 end
655
655
656 def self.set_trac_db_password(password)
656 def self.set_trac_db_password(password)
657 @@trac_db_password = password
657 @@trac_db_password = password
658 end
658 end
659
659
660 def self.set_trac_db_schema(schema)
660 def self.set_trac_db_schema(schema)
661 @@trac_db_schema = schema
661 @@trac_db_schema = schema
662 end
662 end
663
663
664 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
664 mattr_reader :trac_directory, :trac_adapter, :trac_db_host, :trac_db_port, :trac_db_name, :trac_db_schema, :trac_db_username, :trac_db_password
665
665
666 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
666 def self.trac_db_path; "#{trac_directory}/db/trac.db" end
667 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
667 def self.trac_attachments_directory; "#{trac_directory}/attachments" end
668
668
669 def self.target_project_identifier(identifier)
669 def self.target_project_identifier(identifier)
670 project = Project.find_by_identifier(identifier)
670 project = Project.find_by_identifier(identifier)
671 if !project
671 if !project
672 # create the target project
672 # create the target project
673 project = Project.new :name => identifier.humanize,
673 project = Project.new :name => identifier.humanize,
674 :description => ''
674 :description => ''
675 project.identifier = identifier
675 project.identifier = identifier
676 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
676 puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
677 # enable issues and wiki for the created project
677 # enable issues and wiki for the created project
678 project.enabled_module_names = ['issue_tracking', 'wiki']
678 project.enabled_module_names = ['issue_tracking', 'wiki']
679 else
679 else
680 puts
680 puts
681 puts "This project already exists in your Redmine database."
681 puts "This project already exists in your Redmine database."
682 print "Are you sure you want to append data to this project ? [Y/n] "
682 print "Are you sure you want to append data to this project ? [Y/n] "
683 STDOUT.flush
683 STDOUT.flush
684 exit if STDIN.gets.match(/^n$/i)
684 exit if STDIN.gets.match(/^n$/i)
685 end
685 end
686 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
686 project.trackers << TRACKER_BUG unless project.trackers.include?(TRACKER_BUG)
687 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
687 project.trackers << TRACKER_FEATURE unless project.trackers.include?(TRACKER_FEATURE)
688 @target_project = project.new_record? ? nil : project
688 @target_project = project.new_record? ? nil : project
689 @target_project.reload
689 @target_project.reload
690 end
690 end
691
691
692 def self.connection_params
692 def self.connection_params
693 if trac_adapter == 'sqlite3'
693 if trac_adapter == 'sqlite3'
694 {:adapter => 'sqlite3',
694 {:adapter => 'sqlite3',
695 :database => trac_db_path}
695 :database => trac_db_path}
696 else
696 else
697 {:adapter => trac_adapter,
697 {:adapter => trac_adapter,
698 :database => trac_db_name,
698 :database => trac_db_name,
699 :host => trac_db_host,
699 :host => trac_db_host,
700 :port => trac_db_port,
700 :port => trac_db_port,
701 :username => trac_db_username,
701 :username => trac_db_username,
702 :password => trac_db_password,
702 :password => trac_db_password,
703 :schema_search_path => trac_db_schema
703 :schema_search_path => trac_db_schema
704 }
704 }
705 end
705 end
706 end
706 end
707
707
708 def self.establish_connection
708 def self.establish_connection
709 constants.each do |const|
709 constants.each do |const|
710 klass = const_get(const)
710 klass = const_get(const)
711 next unless klass.respond_to? 'establish_connection'
711 next unless klass.respond_to? 'establish_connection'
712 klass.establish_connection connection_params
712 klass.establish_connection connection_params
713 end
713 end
714 end
714 end
715
715
716 private
716 private
717 def self.encode(text)
717 def self.encode(text)
718 @ic.iconv text
718 @ic.iconv text
719 rescue
719 rescue
720 text
720 text
721 end
721 end
722 end
722 end
723
723
724 puts
724 puts
725 if Redmine::DefaultData::Loader.no_data?
725 if Redmine::DefaultData::Loader.no_data?
726 puts "Redmine configuration need to be loaded before importing data."
726 puts "Redmine configuration need to be loaded before importing data."
727 puts "Please, run this first:"
727 puts "Please, run this first:"
728 puts
728 puts
729 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
729 puts " rake redmine:load_default_data RAILS_ENV=\"#{ENV['RAILS_ENV']}\""
730 exit
730 exit
731 end
731 end
732
732
733 puts "WARNING: a new project will be added to Redmine during this process."
733 puts "WARNING: a new project will be added to Redmine during this process."
734 print "Are you sure you want to continue ? [y/N] "
734 print "Are you sure you want to continue ? [y/N] "
735 STDOUT.flush
735 STDOUT.flush
736 break unless STDIN.gets.match(/^y$/i)
736 break unless STDIN.gets.match(/^y$/i)
737 puts
737 puts
738
738
739 def prompt(text, options = {}, &block)
739 def prompt(text, options = {}, &block)
740 default = options[:default] || ''
740 default = options[:default] || ''
741 while true
741 while true
742 print "#{text} [#{default}]: "
742 print "#{text} [#{default}]: "
743 STDOUT.flush
743 STDOUT.flush
744 value = STDIN.gets.chomp!
744 value = STDIN.gets.chomp!
745 value = default if value.blank?
745 value = default if value.blank?
746 break if yield value
746 break if yield value
747 end
747 end
748 end
748 end
749
749
750 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
750 DEFAULT_PORTS = {'mysql' => 3306, 'postgresql' => 5432}
751
751
752 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
752 prompt('Trac directory') {|directory| TracMigrate.set_trac_directory directory.strip}
753 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
753 prompt('Trac database adapter (sqlite3, mysql2, postgresql)', :default => 'sqlite3') {|adapter| TracMigrate.set_trac_adapter adapter}
754 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
754 unless %w(sqlite3).include?(TracMigrate.trac_adapter)
755 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
755 prompt('Trac database host', :default => 'localhost') {|host| TracMigrate.set_trac_db_host host}
756 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
756 prompt('Trac database port', :default => DEFAULT_PORTS[TracMigrate.trac_adapter]) {|port| TracMigrate.set_trac_db_port port}
757 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
757 prompt('Trac database name') {|name| TracMigrate.set_trac_db_name name}
758 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
758 prompt('Trac database schema', :default => 'public') {|schema| TracMigrate.set_trac_db_schema schema}
759 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
759 prompt('Trac database username') {|username| TracMigrate.set_trac_db_username username}
760 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
760 prompt('Trac database password') {|password| TracMigrate.set_trac_db_password password}
761 end
761 end
762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
762 prompt('Trac database encoding', :default => 'UTF-8') {|encoding| TracMigrate.encoding encoding}
763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
763 prompt('Target project identifier') {|identifier| TracMigrate.target_project_identifier identifier}
764 puts
764 puts
765
765
766 # Turn off email notifications
766 # Turn off email notifications
767 Setting.notified_events = []
767 Setting.notified_events = []
768
768
769 TracMigrate.migrate
769 TracMigrate.migrate
770 end
770 end
771 end
771 end
772
772
@@ -1,219 +1,219
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RolesControllerTest < ActionController::TestCase
20 class RolesControllerTest < ActionController::TestCase
21 fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
21 fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
22
22
23 def setup
23 def setup
24 @controller = RolesController.new
24 @controller = RolesController.new
25 @request = ActionController::TestRequest.new
25 @request = ActionController::TestRequest.new
26 @response = ActionController::TestResponse.new
26 @response = ActionController::TestResponse.new
27 User.current = nil
27 User.current = nil
28 @request.session[:user_id] = 1 # admin
28 @request.session[:user_id] = 1 # admin
29 end
29 end
30
30
31 def test_index
31 def test_index
32 get :index
32 get :index
33 assert_response :success
33 assert_response :success
34 assert_template 'index'
34 assert_template 'index'
35
35
36 assert_not_nil assigns(:roles)
36 assert_not_nil assigns(:roles)
37 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
37 assert_equal Role.order('builtin, position').all, assigns(:roles)
38
38
39 assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' },
39 assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' },
40 :content => 'Manager'
40 :content => 'Manager'
41 end
41 end
42
42
43 def test_new
43 def test_new
44 get :new
44 get :new
45 assert_response :success
45 assert_response :success
46 assert_template 'new'
46 assert_template 'new'
47 end
47 end
48
48
49 def test_new_with_copy
49 def test_new_with_copy
50 copy_from = Role.find(2)
50 copy_from = Role.find(2)
51
51
52 get :new, :copy => copy_from.id.to_s
52 get :new, :copy => copy_from.id.to_s
53 assert_response :success
53 assert_response :success
54 assert_template 'new'
54 assert_template 'new'
55
55
56 role = assigns(:role)
56 role = assigns(:role)
57 assert_equal copy_from.permissions, role.permissions
57 assert_equal copy_from.permissions, role.permissions
58
58
59 assert_select 'form' do
59 assert_select 'form' do
60 # blank name
60 # blank name
61 assert_select 'input[name=?][value=]', 'role[name]'
61 assert_select 'input[name=?][value=]', 'role[name]'
62 # edit_project permission checked
62 # edit_project permission checked
63 assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]'
63 assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]'
64 # add_project permission not checked
64 # add_project permission not checked
65 assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]'
65 assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]'
66 assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0
66 assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0
67 # workflow copy selected
67 # workflow copy selected
68 assert_select 'select[name=?]', 'copy_workflow_from' do
68 assert_select 'select[name=?]', 'copy_workflow_from' do
69 assert_select 'option[value=2][selected=selected]'
69 assert_select 'option[value=2][selected=selected]'
70 end
70 end
71 end
71 end
72 end
72 end
73
73
74 def test_create_with_validaton_failure
74 def test_create_with_validaton_failure
75 post :create, :role => {:name => '',
75 post :create, :role => {:name => '',
76 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
76 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
77 :assignable => '0'}
77 :assignable => '0'}
78
78
79 assert_response :success
79 assert_response :success
80 assert_template 'new'
80 assert_template 'new'
81 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
81 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
82 end
82 end
83
83
84 def test_create_without_workflow_copy
84 def test_create_without_workflow_copy
85 post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
85 post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
86 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
86 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
87 :assignable => '0'}
87 :assignable => '0'}
88
88
89 assert_redirected_to '/roles'
89 assert_redirected_to '/roles'
90 role = Role.find_by_name('RoleWithoutWorkflowCopy')
90 role = Role.find_by_name('RoleWithoutWorkflowCopy')
91 assert_not_nil role
91 assert_not_nil role
92 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
92 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
93 assert !role.assignable?
93 assert !role.assignable?
94 end
94 end
95
95
96 def test_create_with_workflow_copy
96 def test_create_with_workflow_copy
97 post :create, :role => {:name => 'RoleWithWorkflowCopy',
97 post :create, :role => {:name => 'RoleWithWorkflowCopy',
98 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
98 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
99 :assignable => '0'},
99 :assignable => '0'},
100 :copy_workflow_from => '1'
100 :copy_workflow_from => '1'
101
101
102 assert_redirected_to '/roles'
102 assert_redirected_to '/roles'
103 role = Role.find_by_name('RoleWithWorkflowCopy')
103 role = Role.find_by_name('RoleWithWorkflowCopy')
104 assert_not_nil role
104 assert_not_nil role
105 assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
105 assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
106 end
106 end
107
107
108 def test_edit
108 def test_edit
109 get :edit, :id => 1
109 get :edit, :id => 1
110 assert_response :success
110 assert_response :success
111 assert_template 'edit'
111 assert_template 'edit'
112 assert_equal Role.find(1), assigns(:role)
112 assert_equal Role.find(1), assigns(:role)
113 assert_select 'select[name=?]', 'role[issues_visibility]'
113 assert_select 'select[name=?]', 'role[issues_visibility]'
114 end
114 end
115
115
116 def test_edit_anonymous
116 def test_edit_anonymous
117 get :edit, :id => Role.anonymous.id
117 get :edit, :id => Role.anonymous.id
118 assert_response :success
118 assert_response :success
119 assert_template 'edit'
119 assert_template 'edit'
120 assert_select 'select[name=?]', 'role[issues_visibility]', 0
120 assert_select 'select[name=?]', 'role[issues_visibility]', 0
121 end
121 end
122
122
123 def test_edit_invalid_should_respond_with_404
123 def test_edit_invalid_should_respond_with_404
124 get :edit, :id => 999
124 get :edit, :id => 999
125 assert_response 404
125 assert_response 404
126 end
126 end
127
127
128 def test_update
128 def test_update
129 put :update, :id => 1,
129 put :update, :id => 1,
130 :role => {:name => 'Manager',
130 :role => {:name => 'Manager',
131 :permissions => ['edit_project', ''],
131 :permissions => ['edit_project', ''],
132 :assignable => '0'}
132 :assignable => '0'}
133
133
134 assert_redirected_to '/roles'
134 assert_redirected_to '/roles'
135 role = Role.find(1)
135 role = Role.find(1)
136 assert_equal [:edit_project], role.permissions
136 assert_equal [:edit_project], role.permissions
137 end
137 end
138
138
139 def test_update_with_failure
139 def test_update_with_failure
140 put :update, :id => 1, :role => {:name => ''}
140 put :update, :id => 1, :role => {:name => ''}
141 assert_response :success
141 assert_response :success
142 assert_template 'edit'
142 assert_template 'edit'
143 end
143 end
144
144
145 def test_destroy
145 def test_destroy
146 r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
146 r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
147
147
148 delete :destroy, :id => r
148 delete :destroy, :id => r
149 assert_redirected_to '/roles'
149 assert_redirected_to '/roles'
150 assert_nil Role.find_by_id(r.id)
150 assert_nil Role.find_by_id(r.id)
151 end
151 end
152
152
153 def test_destroy_role_in_use
153 def test_destroy_role_in_use
154 delete :destroy, :id => 1
154 delete :destroy, :id => 1
155 assert_redirected_to '/roles'
155 assert_redirected_to '/roles'
156 assert_equal 'This role is in use and cannot be deleted.', flash[:error]
156 assert_equal 'This role is in use and cannot be deleted.', flash[:error]
157 assert_not_nil Role.find_by_id(1)
157 assert_not_nil Role.find_by_id(1)
158 end
158 end
159
159
160 def test_get_permissions
160 def test_get_permissions
161 get :permissions
161 get :permissions
162 assert_response :success
162 assert_response :success
163 assert_template 'permissions'
163 assert_template 'permissions'
164
164
165 assert_not_nil assigns(:roles)
165 assert_not_nil assigns(:roles)
166 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
166 assert_equal Role.order('builtin, position').all, assigns(:roles)
167
167
168 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
168 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
169 :name => 'permissions[3][]',
169 :name => 'permissions[3][]',
170 :value => 'add_issues',
170 :value => 'add_issues',
171 :checked => 'checked' }
171 :checked => 'checked' }
172
172
173 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
173 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
174 :name => 'permissions[3][]',
174 :name => 'permissions[3][]',
175 :value => 'delete_issues',
175 :value => 'delete_issues',
176 :checked => nil }
176 :checked => nil }
177 end
177 end
178
178
179 def test_post_permissions
179 def test_post_permissions
180 post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
180 post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
181 assert_redirected_to '/roles'
181 assert_redirected_to '/roles'
182
182
183 assert_equal [:edit_issues], Role.find(1).permissions
183 assert_equal [:edit_issues], Role.find(1).permissions
184 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
184 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
185 assert Role.find(2).permissions.empty?
185 assert Role.find(2).permissions.empty?
186 end
186 end
187
187
188 def test_clear_all_permissions
188 def test_clear_all_permissions
189 post :permissions, :permissions => { '0' => '' }
189 post :permissions, :permissions => { '0' => '' }
190 assert_redirected_to '/roles'
190 assert_redirected_to '/roles'
191 assert Role.find(1).permissions.empty?
191 assert Role.find(1).permissions.empty?
192 end
192 end
193
193
194 def test_move_highest
194 def test_move_highest
195 put :update, :id => 3, :role => {:move_to => 'highest'}
195 put :update, :id => 3, :role => {:move_to => 'highest'}
196 assert_redirected_to '/roles'
196 assert_redirected_to '/roles'
197 assert_equal 1, Role.find(3).position
197 assert_equal 1, Role.find(3).position
198 end
198 end
199
199
200 def test_move_higher
200 def test_move_higher
201 position = Role.find(3).position
201 position = Role.find(3).position
202 put :update, :id => 3, :role => {:move_to => 'higher'}
202 put :update, :id => 3, :role => {:move_to => 'higher'}
203 assert_redirected_to '/roles'
203 assert_redirected_to '/roles'
204 assert_equal position - 1, Role.find(3).position
204 assert_equal position - 1, Role.find(3).position
205 end
205 end
206
206
207 def test_move_lower
207 def test_move_lower
208 position = Role.find(2).position
208 position = Role.find(2).position
209 put :update, :id => 2, :role => {:move_to => 'lower'}
209 put :update, :id => 2, :role => {:move_to => 'lower'}
210 assert_redirected_to '/roles'
210 assert_redirected_to '/roles'
211 assert_equal position + 1, Role.find(2).position
211 assert_equal position + 1, Role.find(2).position
212 end
212 end
213
213
214 def test_move_lowest
214 def test_move_lowest
215 put :update, :id => 2, :role => {:move_to => 'lowest'}
215 put :update, :id => 2, :role => {:move_to => 'lowest'}
216 assert_redirected_to '/roles'
216 assert_redirected_to '/roles'
217 assert_equal Role.count, Role.find(2).position
217 assert_equal Role.count, Role.find(2).position
218 end
218 end
219 end
219 end
@@ -1,312 +1,315
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'workflows_controller'
19 require 'workflows_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class WorkflowsController; def rescue_action(e) raise e end; end
22 class WorkflowsController; def rescue_action(e) raise e end; end
23
23
24 class WorkflowsControllerTest < ActionController::TestCase
24 class WorkflowsControllerTest < ActionController::TestCase
25 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
25 fixtures :roles, :trackers, :workflows, :users, :issue_statuses
26
26
27 def setup
27 def setup
28 @controller = WorkflowsController.new
28 @controller = WorkflowsController.new
29 @request = ActionController::TestRequest.new
29 @request = ActionController::TestRequest.new
30 @response = ActionController::TestResponse.new
30 @response = ActionController::TestResponse.new
31 User.current = nil
31 User.current = nil
32 @request.session[:user_id] = 1 # admin
32 @request.session[:user_id] = 1 # admin
33 end
33 end
34
34
35 def test_index
35 def test_index
36 get :index
36 get :index
37 assert_response :success
37 assert_response :success
38 assert_template 'index'
38 assert_template 'index'
39
39
40 count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
40 count = WorkflowTransition.count(:all, :conditions => 'role_id = 1 AND tracker_id = 2')
41 assert_tag :tag => 'a', :content => count.to_s,
41 assert_tag :tag => 'a', :content => count.to_s,
42 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
42 :attributes => { :href => '/workflows/edit?role_id=1&amp;tracker_id=2' }
43 end
43 end
44
44
45 def test_get_edit
45 def test_get_edit
46 get :edit
46 get :edit
47 assert_response :success
47 assert_response :success
48 assert_template 'edit'
48 assert_template 'edit'
49 assert_not_nil assigns(:roles)
49 assert_not_nil assigns(:roles)
50 assert_not_nil assigns(:trackers)
50 assert_not_nil assigns(:trackers)
51 end
51 end
52
52
53 def test_get_edit_with_role_and_tracker
53 def test_get_edit_with_role_and_tracker
54 WorkflowTransition.delete_all
54 WorkflowTransition.delete_all
55 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
55 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 2, :new_status_id => 3)
56 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
56 WorkflowTransition.create!(:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 5)
57
57
58 get :edit, :role_id => 2, :tracker_id => 1
58 get :edit, :role_id => 2, :tracker_id => 1
59 assert_response :success
59 assert_response :success
60 assert_template 'edit'
60 assert_template 'edit'
61
61
62 # used status only
62 # used status only
63 assert_not_nil assigns(:statuses)
63 assert_not_nil assigns(:statuses)
64 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
64 assert_equal [2, 3, 5], assigns(:statuses).collect(&:id)
65
65
66 # allowed transitions
66 # allowed transitions
67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
67 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
68 :name => 'issue_status[3][5][]',
68 :name => 'issue_status[3][5][]',
69 :value => 'always',
69 :value => 'always',
70 :checked => 'checked' }
70 :checked => 'checked' }
71 # not allowed
71 # not allowed
72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
72 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
73 :name => 'issue_status[3][2][]',
73 :name => 'issue_status[3][2][]',
74 :value => 'always',
74 :value => 'always',
75 :checked => nil }
75 :checked => nil }
76 # unused
76 # unused
77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
77 assert_no_tag :tag => 'input', :attributes => { :type => 'checkbox',
78 :name => 'issue_status[1][1][]' }
78 :name => 'issue_status[1][1][]' }
79 end
79 end
80
80
81 def test_get_edit_with_role_and_tracker_and_all_statuses
81 def test_get_edit_with_role_and_tracker_and_all_statuses
82 WorkflowTransition.delete_all
82 WorkflowTransition.delete_all
83
83
84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
84 get :edit, :role_id => 2, :tracker_id => 1, :used_statuses_only => '0'
85 assert_response :success
85 assert_response :success
86 assert_template 'edit'
86 assert_template 'edit'
87
87
88 assert_not_nil assigns(:statuses)
88 assert_not_nil assigns(:statuses)
89 assert_equal IssueStatus.count, assigns(:statuses).size
89 assert_equal IssueStatus.count, assigns(:statuses).size
90
90
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
91 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
92 :name => 'issue_status[1][1][]',
92 :name => 'issue_status[1][1][]',
93 :value => 'always',
93 :value => 'always',
94 :checked => nil }
94 :checked => nil }
95 end
95 end
96
96
97 def test_post_edit
97 def test_post_edit
98 post :edit, :role_id => 2, :tracker_id => 1,
98 post :edit, :role_id => 2, :tracker_id => 1,
99 :issue_status => {
99 :issue_status => {
100 '4' => {'5' => ['always']},
100 '4' => {'5' => ['always']},
101 '3' => {'1' => ['always'], '2' => ['always']}
101 '3' => {'1' => ['always'], '2' => ['always']}
102 }
102 }
103 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
103 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
104
104
105 assert_equal 3, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
105 assert_equal 3, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
106 assert_not_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
106 assert_not_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
107 assert_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
107 assert_nil WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 5, :new_status_id => 4})
108 end
108 end
109
109
110 def test_post_edit_with_additional_transitions
110 def test_post_edit_with_additional_transitions
111 post :edit, :role_id => 2, :tracker_id => 1,
111 post :edit, :role_id => 2, :tracker_id => 1,
112 :issue_status => {
112 :issue_status => {
113 '4' => {'5' => ['always']},
113 '4' => {'5' => ['always']},
114 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
114 '3' => {'1' => ['author'], '2' => ['assignee'], '4' => ['author', 'assignee']}
115 }
115 }
116 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
116 assert_redirected_to '/workflows/edit?role_id=2&tracker_id=1'
117
117
118 assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
118 assert_equal 4, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
119
119
120 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
120 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 4, :new_status_id => 5})
121 assert ! w.author
121 assert ! w.author
122 assert ! w.assignee
122 assert ! w.assignee
123 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
123 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 1})
124 assert w.author
124 assert w.author
125 assert ! w.assignee
125 assert ! w.assignee
126 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
126 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 2})
127 assert ! w.author
127 assert ! w.author
128 assert w.assignee
128 assert w.assignee
129 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
129 w = WorkflowTransition.find(:first, :conditions => {:role_id => 2, :tracker_id => 1, :old_status_id => 3, :new_status_id => 4})
130 assert w.author
130 assert w.author
131 assert w.assignee
131 assert w.assignee
132 end
132 end
133
133
134 def test_clear_workflow
134 def test_clear_workflow
135 assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
135 assert WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2}) > 0
136
136
137 post :edit, :role_id => 2, :tracker_id => 1
137 post :edit, :role_id => 2, :tracker_id => 1
138 assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
138 assert_equal 0, WorkflowTransition.count(:conditions => {:tracker_id => 1, :role_id => 2})
139 end
139 end
140
140
141 def test_get_permissions
141 def test_get_permissions
142 get :permissions
142 get :permissions
143
143
144 assert_response :success
144 assert_response :success
145 assert_template 'permissions'
145 assert_template 'permissions'
146 assert_not_nil assigns(:roles)
146 assert_not_nil assigns(:roles)
147 assert_not_nil assigns(:trackers)
147 assert_not_nil assigns(:trackers)
148 end
148 end
149
149
150 def test_get_permissions_with_role_and_tracker
150 def test_get_permissions_with_role_and_tracker
151 WorkflowPermission.delete_all
151 WorkflowPermission.delete_all
152 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
152 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
153 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
153 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
154 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
154 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
155
155
156 get :permissions, :role_id => 1, :tracker_id => 2
156 get :permissions, :role_id => 1, :tracker_id => 2
157 assert_response :success
157 assert_response :success
158 assert_template 'permissions'
158 assert_template 'permissions'
159
159
160 assert_select 'input[name=role_id][value=1]'
160 assert_select 'input[name=role_id][value=1]'
161 assert_select 'input[name=tracker_id][value=2]'
161 assert_select 'input[name=tracker_id][value=2]'
162
162
163 # Required field
163 # Required field
164 assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do
164 assert_select 'select[name=?]', 'permissions[assigned_to_id][2]' do
165 assert_select 'option[value=]'
165 assert_select 'option[value=]'
166 assert_select 'option[value=][selected=selected]', 0
166 assert_select 'option[value=][selected=selected]', 0
167 assert_select 'option[value=readonly]', :text => 'Read-only'
167 assert_select 'option[value=readonly]', :text => 'Read-only'
168 assert_select 'option[value=readonly][selected=selected]', 0
168 assert_select 'option[value=readonly][selected=selected]', 0
169 assert_select 'option[value=required]', :text => 'Required'
169 assert_select 'option[value=required]', :text => 'Required'
170 assert_select 'option[value=required][selected=selected]'
170 assert_select 'option[value=required][selected=selected]'
171 end
171 end
172
172
173 # Read-only field
173 # Read-only field
174 assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do
174 assert_select 'select[name=?]', 'permissions[fixed_version_id][3]' do
175 assert_select 'option[value=]'
175 assert_select 'option[value=]'
176 assert_select 'option[value=][selected=selected]', 0
176 assert_select 'option[value=][selected=selected]', 0
177 assert_select 'option[value=readonly]', :text => 'Read-only'
177 assert_select 'option[value=readonly]', :text => 'Read-only'
178 assert_select 'option[value=readonly][selected=selected]'
178 assert_select 'option[value=readonly][selected=selected]'
179 assert_select 'option[value=required]', :text => 'Required'
179 assert_select 'option[value=required]', :text => 'Required'
180 assert_select 'option[value=required][selected=selected]', 0
180 assert_select 'option[value=required][selected=selected]', 0
181 end
181 end
182
182
183 # Other field
183 # Other field
184 assert_select 'select[name=?]', 'permissions[due_date][3]' do
184 assert_select 'select[name=?]', 'permissions[due_date][3]' do
185 assert_select 'option[value=]'
185 assert_select 'option[value=]'
186 assert_select 'option[value=][selected=selected]', 0
186 assert_select 'option[value=][selected=selected]', 0
187 assert_select 'option[value=readonly]', :text => 'Read-only'
187 assert_select 'option[value=readonly]', :text => 'Read-only'
188 assert_select 'option[value=readonly][selected=selected]', 0
188 assert_select 'option[value=readonly][selected=selected]', 0
189 assert_select 'option[value=required]', :text => 'Required'
189 assert_select 'option[value=required]', :text => 'Required'
190 assert_select 'option[value=required][selected=selected]', 0
190 assert_select 'option[value=required][selected=selected]', 0
191 end
191 end
192 end
192 end
193
193
194 def test_get_permissions_with_required_custom_field_should_not_show_required_option
194 def test_get_permissions_with_required_custom_field_should_not_show_required_option
195 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true)
195 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string', :tracker_ids => [1], :is_required => true)
196
196
197 get :permissions, :role_id => 1, :tracker_id => 1
197 get :permissions, :role_id => 1, :tracker_id => 1
198 assert_response :success
198 assert_response :success
199 assert_template 'permissions'
199 assert_template 'permissions'
200
200
201 # Custom field that is always required
201 # Custom field that is always required
202 # The default option is "(Required)"
202 # The default option is "(Required)"
203 assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do
203 assert_select 'select[name=?]', "permissions[#{cf.id}][3]" do
204 assert_select 'option[value=]'
204 assert_select 'option[value=]'
205 assert_select 'option[value=readonly]', :text => 'Read-only'
205 assert_select 'option[value=readonly]', :text => 'Read-only'
206 assert_select 'option[value=required]', 0
206 assert_select 'option[value=required]', 0
207 end
207 end
208 end
208 end
209
209
210 def test_get_permissions_with_role_and_tracker_and_all_statuses
210 def test_get_permissions_with_role_and_tracker_and_all_statuses
211 WorkflowTransition.delete_all
211 WorkflowTransition.delete_all
212
212
213 get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0'
213 get :permissions, :role_id => 1, :tracker_id => 2, :used_statuses_only => '0'
214 assert_response :success
214 assert_response :success
215 assert_equal IssueStatus.sorted.all, assigns(:statuses)
215 assert_equal IssueStatus.sorted.all, assigns(:statuses)
216 end
216 end
217
217
218 def test_post_permissions
218 def test_post_permissions
219 WorkflowPermission.delete_all
219 WorkflowPermission.delete_all
220
220
221 post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
221 post :permissions, :role_id => 1, :tracker_id => 2, :permissions => {
222 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''},
222 'assigned_to_id' => {'1' => '', '2' => 'readonly', '3' => ''},
223 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''},
223 'fixed_version_id' => {'1' => 'required', '2' => 'readonly', '3' => ''},
224 'due_date' => {'1' => '', '2' => '', '3' => ''},
224 'due_date' => {'1' => '', '2' => '', '3' => ''},
225 }
225 }
226 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
226 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
227
227
228 workflows = WorkflowPermission.all
228 workflows = WorkflowPermission.all
229 assert_equal 3, workflows.size
229 assert_equal 3, workflows.size
230 workflows.each do |workflow|
230 workflows.each do |workflow|
231 assert_equal 1, workflow.role_id
231 assert_equal 1, workflow.role_id
232 assert_equal 2, workflow.tracker_id
232 assert_equal 2, workflow.tracker_id
233 end
233 end
234 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
234 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'assigned_to_id' && wf.rule == 'readonly'}
235 assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
235 assert workflows.detect {|wf| wf.old_status_id == 1 && wf.field_name == 'fixed_version_id' && wf.rule == 'required'}
236 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
236 assert workflows.detect {|wf| wf.old_status_id == 2 && wf.field_name == 'fixed_version_id' && wf.rule == 'readonly'}
237 end
237 end
238
238
239 def test_post_permissions_should_clear_permissions
239 def test_post_permissions_should_clear_permissions
240 WorkflowPermission.delete_all
240 WorkflowPermission.delete_all
241 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
241 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'assigned_to_id', :rule => 'required')
242 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
242 WorkflowPermission.create!(:role_id => 1, :tracker_id => 2, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
243 wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
243 wf1 = WorkflowPermission.create!(:role_id => 1, :tracker_id => 3, :old_status_id => 2, :field_name => 'fixed_version_id', :rule => 'required')
244 wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
244 wf2 = WorkflowPermission.create!(:role_id => 2, :tracker_id => 2, :old_status_id => 3, :field_name => 'fixed_version_id', :rule => 'readonly')
245
245
246 post :permissions, :role_id => 1, :tracker_id => 2
246 post :permissions, :role_id => 1, :tracker_id => 2
247 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
247 assert_redirected_to '/workflows/permissions?role_id=1&tracker_id=2'
248
248
249 workflows = WorkflowPermission.all
249 workflows = WorkflowPermission.all
250 assert_equal 2, workflows.size
250 assert_equal 2, workflows.size
251 assert wf1.reload
251 assert wf1.reload
252 assert wf2.reload
252 assert wf2.reload
253 end
253 end
254
254
255 def test_get_copy
255 def test_get_copy
256 get :copy
256 get :copy
257 assert_response :success
257 assert_response :success
258 assert_template 'copy'
258 assert_template 'copy'
259 assert_select 'select[name=source_tracker_id]' do
259 assert_select 'select[name=source_tracker_id]' do
260 assert_select 'option[value=1]', :text => 'Bug'
260 assert_select 'option[value=1]', :text => 'Bug'
261 end
261 end
262 assert_select 'select[name=source_role_id]' do
262 assert_select 'select[name=source_role_id]' do
263 assert_select 'option[value=2]', :text => 'Developer'
263 assert_select 'option[value=2]', :text => 'Developer'
264 end
264 end
265 assert_select 'select[name=?]', 'target_tracker_ids[]' do
265 assert_select 'select[name=?]', 'target_tracker_ids[]' do
266 assert_select 'option[value=3]', :text => 'Support request'
266 assert_select 'option[value=3]', :text => 'Support request'
267 end
267 end
268 assert_select 'select[name=?]', 'target_role_ids[]' do
268 assert_select 'select[name=?]', 'target_role_ids[]' do
269 assert_select 'option[value=1]', :text => 'Manager'
269 assert_select 'option[value=1]', :text => 'Manager'
270 end
270 end
271 end
271 end
272
272
273 def test_post_copy_one_to_one
273 def test_post_copy_one_to_one
274 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
274 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
275
275
276 post :copy, :source_tracker_id => '1', :source_role_id => '2',
276 post :copy, :source_tracker_id => '1', :source_role_id => '2',
277 :target_tracker_ids => ['3'], :target_role_ids => ['1']
277 :target_tracker_ids => ['3'], :target_role_ids => ['1']
278 assert_response 302
278 assert_response 302
279 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
279 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
280 end
280 end
281
281
282 def test_post_copy_one_to_many
282 def test_post_copy_one_to_many
283 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
283 source_transitions = status_transitions(:tracker_id => 1, :role_id => 2)
284
284
285 post :copy, :source_tracker_id => '1', :source_role_id => '2',
285 post :copy, :source_tracker_id => '1', :source_role_id => '2',
286 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
286 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
287 assert_response 302
287 assert_response 302
288 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
288 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 1)
289 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
289 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 1)
290 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
290 assert_equal source_transitions, status_transitions(:tracker_id => 2, :role_id => 3)
291 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
291 assert_equal source_transitions, status_transitions(:tracker_id => 3, :role_id => 3)
292 end
292 end
293
293
294 def test_post_copy_many_to_many
294 def test_post_copy_many_to_many
295 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
295 source_t2 = status_transitions(:tracker_id => 2, :role_id => 2)
296 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
296 source_t3 = status_transitions(:tracker_id => 3, :role_id => 2)
297
297
298 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
298 post :copy, :source_tracker_id => 'any', :source_role_id => '2',
299 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
299 :target_tracker_ids => ['2', '3'], :target_role_ids => ['1', '3']
300 assert_response 302
300 assert_response 302
301 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
301 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 1)
302 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
302 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 1)
303 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
303 assert_equal source_t2, status_transitions(:tracker_id => 2, :role_id => 3)
304 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
304 assert_equal source_t3, status_transitions(:tracker_id => 3, :role_id => 3)
305 end
305 end
306
306
307 # Returns an array of status transitions that can be compared
307 # Returns an array of status transitions that can be compared
308 def status_transitions(conditions)
308 def status_transitions(conditions)
309 WorkflowTransition.find(:all, :conditions => conditions,
309 WorkflowTransition.
310 :order => 'tracker_id, role_id, old_status_id, new_status_id').collect {|w| [w.old_status, w.new_status_id]}
310 where(conditions).
311 order('tracker_id, role_id, old_status_id, new_status_id').
312 all.
313 collect {|w| [w.old_status, w.new_status_id]}
311 end
314 end
312 end
315 end
@@ -1,1164 +1,1164
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include ERB::Util
23 include ERB::Util
24 include Rails.application.routes.url_helpers
24 include Rails.application.routes.url_helpers
25
25
26 fixtures :projects, :roles, :enabled_modules, :users,
26 fixtures :projects, :roles, :enabled_modules, :users,
27 :repositories, :changesets,
27 :repositories, :changesets,
28 :trackers, :issue_statuses, :issues, :versions, :documents,
28 :trackers, :issue_statuses, :issues, :versions, :documents,
29 :wikis, :wiki_pages, :wiki_contents,
29 :wikis, :wiki_pages, :wiki_contents,
30 :boards, :messages, :news,
30 :boards, :messages, :news,
31 :attachments, :enumerations
31 :attachments, :enumerations
32
32
33 def setup
33 def setup
34 super
34 super
35 set_tmp_attachments_directory
35 set_tmp_attachments_directory
36 end
36 end
37
37
38 context "#link_to_if_authorized" do
38 context "#link_to_if_authorized" do
39 context "authorized user" do
39 context "authorized user" do
40 should "be tested"
40 should "be tested"
41 end
41 end
42
42
43 context "unauthorized user" do
43 context "unauthorized user" do
44 should "be tested"
44 should "be tested"
45 end
45 end
46
46
47 should "allow using the :controller and :action for the target link" do
47 should "allow using the :controller and :action for the target link" do
48 User.current = User.find_by_login('admin')
48 User.current = User.find_by_login('admin')
49
49
50 @project = Issue.first.project # Used by helper
50 @project = Issue.first.project # Used by helper
51 response = link_to_if_authorized("By controller/action",
51 response = link_to_if_authorized("By controller/action",
52 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
52 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
53 assert_match /href/, response
53 assert_match /href/, response
54 end
54 end
55
55
56 end
56 end
57
57
58 def test_auto_links
58 def test_auto_links
59 to_test = {
59 to_test = {
60 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
60 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
61 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
61 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
62 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
62 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
63 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
63 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
64 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
64 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
65 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
65 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
66 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
66 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
67 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
67 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
68 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
68 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
69 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
69 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
70 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
70 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
71 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
71 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
72 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
72 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
73 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
73 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
74 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
74 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
75 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
75 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
76 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
76 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
77 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
77 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
78 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
78 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
79 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
79 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
80 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
80 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
81 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
81 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
82 # two exclamation marks
82 # two exclamation marks
83 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
83 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
84 # escaping
84 # escaping
85 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
85 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
86 # wrap in angle brackets
86 # wrap in angle brackets
87 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
87 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
88 }
88 }
89 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
89 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
90 end
90 end
91
91
92 if 'ruby'.respond_to?(:encoding)
92 if 'ruby'.respond_to?(:encoding)
93 def test_auto_links_with_non_ascii_characters
93 def test_auto_links_with_non_ascii_characters
94 to_test = {
94 to_test = {
95 'http://foo.bar/тСст' => '<a class="external" href="http://foo.bar/тСст">http://foo.bar/тСст</a>'
95 'http://foo.bar/тСст' => '<a class="external" href="http://foo.bar/тСст">http://foo.bar/тСст</a>'
96 }
96 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
98 end
99 else
99 else
100 puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version'
100 puts 'Skipping test_auto_links_with_non_ascii_characters, unsupported ruby version'
101 end
101 end
102
102
103 def test_auto_mailto
103 def test_auto_mailto
104 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
104 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
105 textilizable('test@foo.bar')
105 textilizable('test@foo.bar')
106 end
106 end
107
107
108 def test_inline_images
108 def test_inline_images
109 to_test = {
109 to_test = {
110 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
110 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
111 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
111 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
112 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
112 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
113 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
113 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
114 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
114 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
115 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
115 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
116 }
116 }
117 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
117 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
118 end
118 end
119
119
120 def test_inline_images_inside_tags
120 def test_inline_images_inside_tags
121 raw = <<-RAW
121 raw = <<-RAW
122 h1. !foo.png! Heading
122 h1. !foo.png! Heading
123
123
124 Centered image:
124 Centered image:
125
125
126 p=. !bar.gif!
126 p=. !bar.gif!
127 RAW
127 RAW
128
128
129 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
129 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
130 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
130 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
131 end
131 end
132
132
133 def test_attached_images
133 def test_attached_images
134 to_test = {
134 to_test = {
135 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
135 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
136 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
136 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
137 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
137 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
138 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
138 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
139 # link image
139 # link image
140 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
140 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
141 }
141 }
142 attachments = Attachment.find(:all)
142 attachments = Attachment.all
143 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
143 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
144 end
144 end
145
145
146 def test_attached_images_filename_extension
146 def test_attached_images_filename_extension
147 set_tmp_attachments_directory
147 set_tmp_attachments_directory
148 a1 = Attachment.new(
148 a1 = Attachment.new(
149 :container => Issue.find(1),
149 :container => Issue.find(1),
150 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
150 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
151 :author => User.find(1))
151 :author => User.find(1))
152 assert a1.save
152 assert a1.save
153 assert_equal "testtest.JPG", a1.filename
153 assert_equal "testtest.JPG", a1.filename
154 assert_equal "image/jpeg", a1.content_type
154 assert_equal "image/jpeg", a1.content_type
155 assert a1.image?
155 assert a1.image?
156
156
157 a2 = Attachment.new(
157 a2 = Attachment.new(
158 :container => Issue.find(1),
158 :container => Issue.find(1),
159 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
159 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
160 :author => User.find(1))
160 :author => User.find(1))
161 assert a2.save
161 assert a2.save
162 assert_equal "testtest.jpeg", a2.filename
162 assert_equal "testtest.jpeg", a2.filename
163 assert_equal "image/jpeg", a2.content_type
163 assert_equal "image/jpeg", a2.content_type
164 assert a2.image?
164 assert a2.image?
165
165
166 a3 = Attachment.new(
166 a3 = Attachment.new(
167 :container => Issue.find(1),
167 :container => Issue.find(1),
168 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
168 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
169 :author => User.find(1))
169 :author => User.find(1))
170 assert a3.save
170 assert a3.save
171 assert_equal "testtest.JPE", a3.filename
171 assert_equal "testtest.JPE", a3.filename
172 assert_equal "image/jpeg", a3.content_type
172 assert_equal "image/jpeg", a3.content_type
173 assert a3.image?
173 assert a3.image?
174
174
175 a4 = Attachment.new(
175 a4 = Attachment.new(
176 :container => Issue.find(1),
176 :container => Issue.find(1),
177 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
177 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
178 :author => User.find(1))
178 :author => User.find(1))
179 assert a4.save
179 assert a4.save
180 assert_equal "Testtest.BMP", a4.filename
180 assert_equal "Testtest.BMP", a4.filename
181 assert_equal "image/x-ms-bmp", a4.content_type
181 assert_equal "image/x-ms-bmp", a4.content_type
182 assert a4.image?
182 assert a4.image?
183
183
184 to_test = {
184 to_test = {
185 'Inline image: !testtest.jpg!' =>
185 'Inline image: !testtest.jpg!' =>
186 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
186 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
187 'Inline image: !testtest.jpeg!' =>
187 'Inline image: !testtest.jpeg!' =>
188 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
188 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
189 'Inline image: !testtest.jpe!' =>
189 'Inline image: !testtest.jpe!' =>
190 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
190 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
191 'Inline image: !testtest.bmp!' =>
191 'Inline image: !testtest.bmp!' =>
192 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
192 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
193 }
193 }
194
194
195 attachments = [a1, a2, a3, a4]
195 attachments = [a1, a2, a3, a4]
196 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
196 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
197 end
197 end
198
198
199 def test_attached_images_should_read_later
199 def test_attached_images_should_read_later
200 set_fixtures_attachments_directory
200 set_fixtures_attachments_directory
201 a1 = Attachment.find(16)
201 a1 = Attachment.find(16)
202 assert_equal "testfile.png", a1.filename
202 assert_equal "testfile.png", a1.filename
203 assert a1.readable?
203 assert a1.readable?
204 assert (! a1.visible?(User.anonymous))
204 assert (! a1.visible?(User.anonymous))
205 assert a1.visible?(User.find(2))
205 assert a1.visible?(User.find(2))
206 a2 = Attachment.find(17)
206 a2 = Attachment.find(17)
207 assert_equal "testfile.PNG", a2.filename
207 assert_equal "testfile.PNG", a2.filename
208 assert a2.readable?
208 assert a2.readable?
209 assert (! a2.visible?(User.anonymous))
209 assert (! a2.visible?(User.anonymous))
210 assert a2.visible?(User.find(2))
210 assert a2.visible?(User.find(2))
211 assert a1.created_on < a2.created_on
211 assert a1.created_on < a2.created_on
212
212
213 to_test = {
213 to_test = {
214 'Inline image: !testfile.png!' =>
214 'Inline image: !testfile.png!' =>
215 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
215 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
216 'Inline image: !Testfile.PNG!' =>
216 'Inline image: !Testfile.PNG!' =>
217 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
217 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
218 }
218 }
219 attachments = [a1, a2]
219 attachments = [a1, a2]
220 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
220 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
221 set_tmp_attachments_directory
221 set_tmp_attachments_directory
222 end
222 end
223
223
224 def test_textile_external_links
224 def test_textile_external_links
225 to_test = {
225 to_test = {
226 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
226 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
227 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
227 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
228 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
228 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
229 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
229 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
230 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
230 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
231 # no multiline link text
231 # no multiline link text
232 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
232 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
233 # mailto link
233 # mailto link
234 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
234 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
235 # two exclamation marks
235 # two exclamation marks
236 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
236 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
237 # escaping
237 # escaping
238 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
238 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
239 }
239 }
240 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
240 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
241 end
241 end
242
242
243 if 'ruby'.respond_to?(:encoding)
243 if 'ruby'.respond_to?(:encoding)
244 def test_textile_external_links_with_non_ascii_characters
244 def test_textile_external_links_with_non_ascii_characters
245 to_test = {
245 to_test = {
246 'This is a "link":http://foo.bar/тСст' => 'This is a <a href="http://foo.bar/тСст" class="external">link</a>'
246 'This is a "link":http://foo.bar/тСст' => 'This is a <a href="http://foo.bar/тСст" class="external">link</a>'
247 }
247 }
248 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
248 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
249 end
249 end
250 else
250 else
251 puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version'
251 puts 'Skipping test_textile_external_links_with_non_ascii_characters, unsupported ruby version'
252 end
252 end
253
253
254 def test_redmine_links
254 def test_redmine_links
255 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
255 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
256 :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)')
256 :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)')
257 note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
257 note_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
258 :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)')
258 :class => 'issue status-1 priority-4 priority-lowest overdue', :title => 'Error 281 when updating a recipe (New)')
259
259
260 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
260 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
261 :class => 'changeset', :title => 'My very first commit')
261 :class => 'changeset', :title => 'My very first commit')
262 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
262 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
263 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
263 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
264
264
265 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
265 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
266 :class => 'document')
266 :class => 'document')
267
267
268 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
268 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
269 :class => 'version')
269 :class => 'version')
270
270
271 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
271 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
272
272
273 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
273 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
274
274
275 news_url = {:controller => 'news', :action => 'show', :id => 1}
275 news_url = {:controller => 'news', :action => 'show', :id => 1}
276
276
277 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
277 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
278
278
279 source_url = '/projects/ecookbook/repository/entry/some/file'
279 source_url = '/projects/ecookbook/repository/entry/some/file'
280 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
280 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
281 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
281 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
282 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
282 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
283
283
284 export_url = '/projects/ecookbook/repository/raw/some/file'
284 export_url = '/projects/ecookbook/repository/raw/some/file'
285 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
285 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
286 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
286 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
287 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
287 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
288
288
289 to_test = {
289 to_test = {
290 # tickets
290 # tickets
291 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
291 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
292 # ticket notes
292 # ticket notes
293 '#3-14' => note_link,
293 '#3-14' => note_link,
294 '#3#note-14' => note_link,
294 '#3#note-14' => note_link,
295 # should not ignore leading zero
295 # should not ignore leading zero
296 '#03' => '#03',
296 '#03' => '#03',
297 # changesets
297 # changesets
298 'r1' => changeset_link,
298 'r1' => changeset_link,
299 'r1.' => "#{changeset_link}.",
299 'r1.' => "#{changeset_link}.",
300 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
300 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
301 'r1,r2' => "#{changeset_link},#{changeset_link2}",
301 'r1,r2' => "#{changeset_link},#{changeset_link2}",
302 # documents
302 # documents
303 'document#1' => document_link,
303 'document#1' => document_link,
304 'document:"Test document"' => document_link,
304 'document:"Test document"' => document_link,
305 # versions
305 # versions
306 'version#2' => version_link,
306 'version#2' => version_link,
307 'version:1.0' => version_link,
307 'version:1.0' => version_link,
308 'version:"1.0"' => version_link,
308 'version:"1.0"' => version_link,
309 # source
309 # source
310 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
310 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
311 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
311 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
312 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
312 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
313 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
313 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
314 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
314 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
315 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
315 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
316 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
316 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
317 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
317 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
318 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
318 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
319 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
319 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
320 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
320 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
321 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
321 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
322 # export
322 # export
323 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
323 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
324 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
324 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
325 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
325 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
326 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
326 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
327 # forum
327 # forum
328 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
328 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
329 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
329 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
330 # message
330 # message
331 'message#4' => link_to('Post 2', message_url, :class => 'message'),
331 'message#4' => link_to('Post 2', message_url, :class => 'message'),
332 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
332 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
333 # news
333 # news
334 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
334 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
335 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
335 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
336 # project
336 # project
337 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
337 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
338 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
338 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
339 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
339 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
340 # not found
340 # not found
341 '#0123456789' => '#0123456789',
341 '#0123456789' => '#0123456789',
342 # invalid expressions
342 # invalid expressions
343 'source:' => 'source:',
343 'source:' => 'source:',
344 # url hash
344 # url hash
345 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
345 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
346 }
346 }
347 @project = Project.find(1)
347 @project = Project.find(1)
348 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
348 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
349 end
349 end
350
350
351 def test_escaped_redmine_links_should_not_be_parsed
351 def test_escaped_redmine_links_should_not_be_parsed
352 to_test = [
352 to_test = [
353 '#3.',
353 '#3.',
354 '#3-14.',
354 '#3-14.',
355 '#3#-note14.',
355 '#3#-note14.',
356 'r1',
356 'r1',
357 'document#1',
357 'document#1',
358 'document:"Test document"',
358 'document:"Test document"',
359 'version#2',
359 'version#2',
360 'version:1.0',
360 'version:1.0',
361 'version:"1.0"',
361 'version:"1.0"',
362 'source:/some/file'
362 'source:/some/file'
363 ]
363 ]
364 @project = Project.find(1)
364 @project = Project.find(1)
365 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
365 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
366 end
366 end
367
367
368 def test_cross_project_redmine_links
368 def test_cross_project_redmine_links
369 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
369 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
370 :class => 'source')
370 :class => 'source')
371
371
372 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
372 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
373 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
373 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
374
374
375 to_test = {
375 to_test = {
376 # documents
376 # documents
377 'document:"Test document"' => 'document:"Test document"',
377 'document:"Test document"' => 'document:"Test document"',
378 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
378 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
379 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
379 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
380 # versions
380 # versions
381 'version:"1.0"' => 'version:"1.0"',
381 'version:"1.0"' => 'version:"1.0"',
382 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
382 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
383 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
383 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
384 # changeset
384 # changeset
385 'r2' => 'r2',
385 'r2' => 'r2',
386 'ecookbook:r2' => changeset_link,
386 'ecookbook:r2' => changeset_link,
387 'invalid:r2' => 'invalid:r2',
387 'invalid:r2' => 'invalid:r2',
388 # source
388 # source
389 'source:/some/file' => 'source:/some/file',
389 'source:/some/file' => 'source:/some/file',
390 'ecookbook:source:/some/file' => source_link,
390 'ecookbook:source:/some/file' => source_link,
391 'invalid:source:/some/file' => 'invalid:source:/some/file',
391 'invalid:source:/some/file' => 'invalid:source:/some/file',
392 }
392 }
393 @project = Project.find(3)
393 @project = Project.find(3)
394 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
394 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
395 end
395 end
396
396
397 def test_multiple_repositories_redmine_links
397 def test_multiple_repositories_redmine_links
398 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
398 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
399 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
399 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
400 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
400 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
401 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
401 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
402
402
403 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
403 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
404 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
404 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
405 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
405 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
406 :class => 'changeset', :title => '')
406 :class => 'changeset', :title => '')
407 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
407 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
408 :class => 'changeset', :title => '')
408 :class => 'changeset', :title => '')
409
409
410 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
410 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
411 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
411 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
412
412
413 to_test = {
413 to_test = {
414 'r2' => changeset_link,
414 'r2' => changeset_link,
415 'svn1|r123' => svn_changeset_link,
415 'svn1|r123' => svn_changeset_link,
416 'invalid|r123' => 'invalid|r123',
416 'invalid|r123' => 'invalid|r123',
417 'commit:hg1|abcd' => hg_changeset_link,
417 'commit:hg1|abcd' => hg_changeset_link,
418 'commit:invalid|abcd' => 'commit:invalid|abcd',
418 'commit:invalid|abcd' => 'commit:invalid|abcd',
419 # source
419 # source
420 'source:some/file' => source_link,
420 'source:some/file' => source_link,
421 'source:hg1|some/file' => hg_source_link,
421 'source:hg1|some/file' => hg_source_link,
422 'source:invalid|some/file' => 'source:invalid|some/file',
422 'source:invalid|some/file' => 'source:invalid|some/file',
423 }
423 }
424
424
425 @project = Project.find(1)
425 @project = Project.find(1)
426 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
426 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
427 end
427 end
428
428
429 def test_cross_project_multiple_repositories_redmine_links
429 def test_cross_project_multiple_repositories_redmine_links
430 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
430 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
431 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
431 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
432 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
432 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
433 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
433 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
434
434
435 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
435 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
436 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
436 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
437 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
437 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
438 :class => 'changeset', :title => '')
438 :class => 'changeset', :title => '')
439 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
439 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
440 :class => 'changeset', :title => '')
440 :class => 'changeset', :title => '')
441
441
442 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
442 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
443 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
443 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
444
444
445 to_test = {
445 to_test = {
446 'ecookbook:r2' => changeset_link,
446 'ecookbook:r2' => changeset_link,
447 'ecookbook:svn1|r123' => svn_changeset_link,
447 'ecookbook:svn1|r123' => svn_changeset_link,
448 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
448 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
449 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
449 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
450 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
450 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
451 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
451 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
452 # source
452 # source
453 'ecookbook:source:some/file' => source_link,
453 'ecookbook:source:some/file' => source_link,
454 'ecookbook:source:hg1|some/file' => hg_source_link,
454 'ecookbook:source:hg1|some/file' => hg_source_link,
455 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
455 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
456 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
456 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
457 }
457 }
458
458
459 @project = Project.find(3)
459 @project = Project.find(3)
460 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
460 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
461 end
461 end
462
462
463 def test_redmine_links_git_commit
463 def test_redmine_links_git_commit
464 changeset_link = link_to('abcd',
464 changeset_link = link_to('abcd',
465 {
465 {
466 :controller => 'repositories',
466 :controller => 'repositories',
467 :action => 'revision',
467 :action => 'revision',
468 :id => 'subproject1',
468 :id => 'subproject1',
469 :rev => 'abcd',
469 :rev => 'abcd',
470 },
470 },
471 :class => 'changeset', :title => 'test commit')
471 :class => 'changeset', :title => 'test commit')
472 to_test = {
472 to_test = {
473 'commit:abcd' => changeset_link,
473 'commit:abcd' => changeset_link,
474 }
474 }
475 @project = Project.find(3)
475 @project = Project.find(3)
476 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
476 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
477 assert r
477 assert r
478 c = Changeset.new(:repository => r,
478 c = Changeset.new(:repository => r,
479 :committed_on => Time.now,
479 :committed_on => Time.now,
480 :revision => 'abcd',
480 :revision => 'abcd',
481 :scmid => 'abcd',
481 :scmid => 'abcd',
482 :comments => 'test commit')
482 :comments => 'test commit')
483 assert( c.save )
483 assert( c.save )
484 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
484 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
485 end
485 end
486
486
487 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
487 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
488 def test_redmine_links_darcs_commit
488 def test_redmine_links_darcs_commit
489 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
489 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
490 {
490 {
491 :controller => 'repositories',
491 :controller => 'repositories',
492 :action => 'revision',
492 :action => 'revision',
493 :id => 'subproject1',
493 :id => 'subproject1',
494 :rev => '123',
494 :rev => '123',
495 },
495 },
496 :class => 'changeset', :title => 'test commit')
496 :class => 'changeset', :title => 'test commit')
497 to_test = {
497 to_test = {
498 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
498 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
499 }
499 }
500 @project = Project.find(3)
500 @project = Project.find(3)
501 r = Repository::Darcs.create!(
501 r = Repository::Darcs.create!(
502 :project => @project, :url => '/tmp/test/darcs',
502 :project => @project, :url => '/tmp/test/darcs',
503 :log_encoding => 'UTF-8')
503 :log_encoding => 'UTF-8')
504 assert r
504 assert r
505 c = Changeset.new(:repository => r,
505 c = Changeset.new(:repository => r,
506 :committed_on => Time.now,
506 :committed_on => Time.now,
507 :revision => '123',
507 :revision => '123',
508 :scmid => '20080308225258-98289-abcd456efg.gz',
508 :scmid => '20080308225258-98289-abcd456efg.gz',
509 :comments => 'test commit')
509 :comments => 'test commit')
510 assert( c.save )
510 assert( c.save )
511 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
511 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
512 end
512 end
513
513
514 def test_redmine_links_mercurial_commit
514 def test_redmine_links_mercurial_commit
515 changeset_link_rev = link_to('r123',
515 changeset_link_rev = link_to('r123',
516 {
516 {
517 :controller => 'repositories',
517 :controller => 'repositories',
518 :action => 'revision',
518 :action => 'revision',
519 :id => 'subproject1',
519 :id => 'subproject1',
520 :rev => '123' ,
520 :rev => '123' ,
521 },
521 },
522 :class => 'changeset', :title => 'test commit')
522 :class => 'changeset', :title => 'test commit')
523 changeset_link_commit = link_to('abcd',
523 changeset_link_commit = link_to('abcd',
524 {
524 {
525 :controller => 'repositories',
525 :controller => 'repositories',
526 :action => 'revision',
526 :action => 'revision',
527 :id => 'subproject1',
527 :id => 'subproject1',
528 :rev => 'abcd' ,
528 :rev => 'abcd' ,
529 },
529 },
530 :class => 'changeset', :title => 'test commit')
530 :class => 'changeset', :title => 'test commit')
531 to_test = {
531 to_test = {
532 'r123' => changeset_link_rev,
532 'r123' => changeset_link_rev,
533 'commit:abcd' => changeset_link_commit,
533 'commit:abcd' => changeset_link_commit,
534 }
534 }
535 @project = Project.find(3)
535 @project = Project.find(3)
536 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
536 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
537 assert r
537 assert r
538 c = Changeset.new(:repository => r,
538 c = Changeset.new(:repository => r,
539 :committed_on => Time.now,
539 :committed_on => Time.now,
540 :revision => '123',
540 :revision => '123',
541 :scmid => 'abcd',
541 :scmid => 'abcd',
542 :comments => 'test commit')
542 :comments => 'test commit')
543 assert( c.save )
543 assert( c.save )
544 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
544 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
545 end
545 end
546
546
547 def test_attachment_links
547 def test_attachment_links
548 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
548 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
549 to_test = {
549 to_test = {
550 'attachment:error281.txt' => attachment_link
550 'attachment:error281.txt' => attachment_link
551 }
551 }
552 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
552 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
553 end
553 end
554
554
555 def test_wiki_links
555 def test_wiki_links
556 to_test = {
556 to_test = {
557 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
557 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
558 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
558 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
559 # title content should be formatted
559 # title content should be formatted
560 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
560 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
561 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
561 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
562 # link with anchor
562 # link with anchor
563 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
563 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
564 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
564 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
565 # UTF8 anchor
565 # UTF8 anchor
566 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
566 '[[Another_page#ВСст|ВСст]]' => %|<a href="/projects/ecookbook/wiki/Another_page##{CGI.escape 'ВСст'}" class="wiki-page">ВСст</a>|,
567 # page that doesn't exist
567 # page that doesn't exist
568 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
568 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
569 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
569 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
570 # link to another project wiki
570 # link to another project wiki
571 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
571 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
572 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
572 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
573 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
573 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
574 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
574 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
575 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
575 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
576 # striked through link
576 # striked through link
577 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
577 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
578 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
578 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
579 # escaping
579 # escaping
580 '![[Another page|Page]]' => '[[Another page|Page]]',
580 '![[Another page|Page]]' => '[[Another page|Page]]',
581 # project does not exist
581 # project does not exist
582 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
582 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
583 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
583 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
584 }
584 }
585
585
586 @project = Project.find(1)
586 @project = Project.find(1)
587 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
587 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
588 end
588 end
589
589
590 def test_wiki_links_within_local_file_generation_context
590 def test_wiki_links_within_local_file_generation_context
591
591
592 to_test = {
592 to_test = {
593 # link to a page
593 # link to a page
594 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
594 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
595 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
595 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
596 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
596 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
597 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
597 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
598 # page that doesn't exist
598 # page that doesn't exist
599 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
599 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
600 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
600 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
601 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
601 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
602 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
602 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
603 }
603 }
604
604
605 @project = Project.find(1)
605 @project = Project.find(1)
606
606
607 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
607 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
608 end
608 end
609
609
610 def test_wiki_links_within_wiki_page_context
610 def test_wiki_links_within_wiki_page_context
611
611
612 page = WikiPage.find_by_title('Another_page' )
612 page = WikiPage.find_by_title('Another_page' )
613
613
614 to_test = {
614 to_test = {
615 # link to another page
615 # link to another page
616 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
616 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
617 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
617 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
618 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
618 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
619 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
619 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
620 # link to the current page
620 # link to the current page
621 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
621 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
622 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
622 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
623 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
623 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
624 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
624 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
625 # page that doesn't exist
625 # page that doesn't exist
626 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
626 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
627 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
627 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
628 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
628 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
629 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
629 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
630 }
630 }
631
631
632 @project = Project.find(1)
632 @project = Project.find(1)
633
633
634 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
634 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
635 end
635 end
636
636
637 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
637 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
638
638
639 to_test = {
639 to_test = {
640 # link to a page
640 # link to a page
641 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
641 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
642 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
642 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
643 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
643 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
644 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
644 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
645 # page that doesn't exist
645 # page that doesn't exist
646 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
646 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
647 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
647 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
648 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
648 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
649 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
649 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
650 }
650 }
651
651
652 @project = Project.find(1)
652 @project = Project.find(1)
653
653
654 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
654 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
655 end
655 end
656
656
657 def test_html_tags
657 def test_html_tags
658 to_test = {
658 to_test = {
659 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
659 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
660 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
660 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
661 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
661 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
662 # do not escape pre/code tags
662 # do not escape pre/code tags
663 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
663 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
664 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
664 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
665 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
665 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
666 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
666 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
667 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
667 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
668 # remove attributes except class
668 # remove attributes except class
669 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
669 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
670 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
670 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
671 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
671 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
672 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
672 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
673 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
673 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
674 # xss
674 # xss
675 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
675 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
676 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
676 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
677 }
677 }
678 to_test.each { |text, result| assert_equal result, textilizable(text) }
678 to_test.each { |text, result| assert_equal result, textilizable(text) }
679 end
679 end
680
680
681 def test_allowed_html_tags
681 def test_allowed_html_tags
682 to_test = {
682 to_test = {
683 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
683 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
684 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
684 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
685 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
685 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
686 }
686 }
687 to_test.each { |text, result| assert_equal result, textilizable(text) }
687 to_test.each { |text, result| assert_equal result, textilizable(text) }
688 end
688 end
689
689
690 def test_pre_tags
690 def test_pre_tags
691 raw = <<-RAW
691 raw = <<-RAW
692 Before
692 Before
693
693
694 <pre>
694 <pre>
695 <prepared-statement-cache-size>32</prepared-statement-cache-size>
695 <prepared-statement-cache-size>32</prepared-statement-cache-size>
696 </pre>
696 </pre>
697
697
698 After
698 After
699 RAW
699 RAW
700
700
701 expected = <<-EXPECTED
701 expected = <<-EXPECTED
702 <p>Before</p>
702 <p>Before</p>
703 <pre>
703 <pre>
704 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
704 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
705 </pre>
705 </pre>
706 <p>After</p>
706 <p>After</p>
707 EXPECTED
707 EXPECTED
708
708
709 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
709 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
710 end
710 end
711
711
712 def test_pre_content_should_not_parse_wiki_and_redmine_links
712 def test_pre_content_should_not_parse_wiki_and_redmine_links
713 raw = <<-RAW
713 raw = <<-RAW
714 [[CookBook documentation]]
714 [[CookBook documentation]]
715
715
716 #1
716 #1
717
717
718 <pre>
718 <pre>
719 [[CookBook documentation]]
719 [[CookBook documentation]]
720
720
721 #1
721 #1
722 </pre>
722 </pre>
723 RAW
723 RAW
724
724
725 expected = <<-EXPECTED
725 expected = <<-EXPECTED
726 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
726 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
727 <p><a href="/issues/1" class="issue status-1 priority-4 priority-lowest" title="Can&#x27;t print recipes (New)">#1</a></p>
727 <p><a href="/issues/1" class="issue status-1 priority-4 priority-lowest" title="Can&#x27;t print recipes (New)">#1</a></p>
728 <pre>
728 <pre>
729 [[CookBook documentation]]
729 [[CookBook documentation]]
730
730
731 #1
731 #1
732 </pre>
732 </pre>
733 EXPECTED
733 EXPECTED
734
734
735 @project = Project.find(1)
735 @project = Project.find(1)
736 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
736 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
737 end
737 end
738
738
739 def test_non_closing_pre_blocks_should_be_closed
739 def test_non_closing_pre_blocks_should_be_closed
740 raw = <<-RAW
740 raw = <<-RAW
741 <pre><code>
741 <pre><code>
742 RAW
742 RAW
743
743
744 expected = <<-EXPECTED
744 expected = <<-EXPECTED
745 <pre><code>
745 <pre><code>
746 </code></pre>
746 </code></pre>
747 EXPECTED
747 EXPECTED
748
748
749 @project = Project.find(1)
749 @project = Project.find(1)
750 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
750 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
751 end
751 end
752
752
753 def test_syntax_highlight
753 def test_syntax_highlight
754 raw = <<-RAW
754 raw = <<-RAW
755 <pre><code class="ruby">
755 <pre><code class="ruby">
756 # Some ruby code here
756 # Some ruby code here
757 </code></pre>
757 </code></pre>
758 RAW
758 RAW
759
759
760 expected = <<-EXPECTED
760 expected = <<-EXPECTED
761 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
761 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
762 </code></pre>
762 </code></pre>
763 EXPECTED
763 EXPECTED
764
764
765 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
765 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
766 end
766 end
767
767
768 def test_to_path_param
768 def test_to_path_param
769 assert_equal 'test1/test2', to_path_param('test1/test2')
769 assert_equal 'test1/test2', to_path_param('test1/test2')
770 assert_equal 'test1/test2', to_path_param('/test1/test2/')
770 assert_equal 'test1/test2', to_path_param('/test1/test2/')
771 assert_equal 'test1/test2', to_path_param('//test1/test2/')
771 assert_equal 'test1/test2', to_path_param('//test1/test2/')
772 assert_equal nil, to_path_param('/')
772 assert_equal nil, to_path_param('/')
773 end
773 end
774
774
775 def test_wiki_links_in_tables
775 def test_wiki_links_in_tables
776 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
776 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
777 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
777 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
778 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
778 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
779 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
779 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
780 }
780 }
781 @project = Project.find(1)
781 @project = Project.find(1)
782 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
782 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
783 end
783 end
784
784
785 def test_text_formatting
785 def test_text_formatting
786 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
786 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
787 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
787 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
788 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
788 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
789 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
789 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
790 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
790 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
791 }
791 }
792 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
792 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
793 end
793 end
794
794
795 def test_wiki_horizontal_rule
795 def test_wiki_horizontal_rule
796 assert_equal '<hr />', textilizable('---')
796 assert_equal '<hr />', textilizable('---')
797 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
797 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
798 end
798 end
799
799
800 def test_footnotes
800 def test_footnotes
801 raw = <<-RAW
801 raw = <<-RAW
802 This is some text[1].
802 This is some text[1].
803
803
804 fn1. This is the foot note
804 fn1. This is the foot note
805 RAW
805 RAW
806
806
807 expected = <<-EXPECTED
807 expected = <<-EXPECTED
808 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
808 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
809 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
809 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
810 EXPECTED
810 EXPECTED
811
811
812 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
812 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
813 end
813 end
814
814
815 def test_headings
815 def test_headings
816 raw = 'h1. Some heading'
816 raw = 'h1. Some heading'
817 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
817 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
818
818
819 assert_equal expected, textilizable(raw)
819 assert_equal expected, textilizable(raw)
820 end
820 end
821
821
822 def test_headings_with_special_chars
822 def test_headings_with_special_chars
823 # This test makes sure that the generated anchor names match the expected
823 # This test makes sure that the generated anchor names match the expected
824 # ones even if the heading text contains unconventional characters
824 # ones even if the heading text contains unconventional characters
825 raw = 'h1. Some heading related to version 0.5'
825 raw = 'h1. Some heading related to version 0.5'
826 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
826 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
827 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
827 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
828
828
829 assert_equal expected, textilizable(raw)
829 assert_equal expected, textilizable(raw)
830 end
830 end
831
831
832 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
832 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
833 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
833 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
834 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
834 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
835
835
836 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
836 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
837
837
838 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
838 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
839 end
839 end
840
840
841 def test_table_of_content
841 def test_table_of_content
842 raw = <<-RAW
842 raw = <<-RAW
843 {{toc}}
843 {{toc}}
844
844
845 h1. Title
845 h1. Title
846
846
847 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
847 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
848
848
849 h2. Subtitle with a [[Wiki]] link
849 h2. Subtitle with a [[Wiki]] link
850
850
851 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
851 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
852
852
853 h2. Subtitle with [[Wiki|another Wiki]] link
853 h2. Subtitle with [[Wiki|another Wiki]] link
854
854
855 h2. Subtitle with %{color:red}red text%
855 h2. Subtitle with %{color:red}red text%
856
856
857 <pre>
857 <pre>
858 some code
858 some code
859 </pre>
859 </pre>
860
860
861 h3. Subtitle with *some* _modifiers_
861 h3. Subtitle with *some* _modifiers_
862
862
863 h3. Subtitle with @inline code@
863 h3. Subtitle with @inline code@
864
864
865 h1. Another title
865 h1. Another title
866
866
867 h3. An "Internet link":http://www.redmine.org/ inside subtitle
867 h3. An "Internet link":http://www.redmine.org/ inside subtitle
868
868
869 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
869 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
870
870
871 RAW
871 RAW
872
872
873 expected = '<ul class="toc">' +
873 expected = '<ul class="toc">' +
874 '<li><a href="#Title">Title</a>' +
874 '<li><a href="#Title">Title</a>' +
875 '<ul>' +
875 '<ul>' +
876 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
876 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
877 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
877 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
878 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
878 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
879 '<ul>' +
879 '<ul>' +
880 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
880 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
881 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
881 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
882 '</ul>' +
882 '</ul>' +
883 '</li>' +
883 '</li>' +
884 '</ul>' +
884 '</ul>' +
885 '</li>' +
885 '</li>' +
886 '<li><a href="#Another-title">Another title</a>' +
886 '<li><a href="#Another-title">Another title</a>' +
887 '<ul>' +
887 '<ul>' +
888 '<li>' +
888 '<li>' +
889 '<ul>' +
889 '<ul>' +
890 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
890 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
891 '</ul>' +
891 '</ul>' +
892 '</li>' +
892 '</li>' +
893 '<li><a href="#Project-Name">Project Name</a></li>' +
893 '<li><a href="#Project-Name">Project Name</a></li>' +
894 '</ul>' +
894 '</ul>' +
895 '</li>' +
895 '</li>' +
896 '</ul>'
896 '</ul>'
897
897
898 @project = Project.find(1)
898 @project = Project.find(1)
899 assert textilizable(raw).gsub("\n", "").include?(expected)
899 assert textilizable(raw).gsub("\n", "").include?(expected)
900 end
900 end
901
901
902 def test_table_of_content_should_generate_unique_anchors
902 def test_table_of_content_should_generate_unique_anchors
903 raw = <<-RAW
903 raw = <<-RAW
904 {{toc}}
904 {{toc}}
905
905
906 h1. Title
906 h1. Title
907
907
908 h2. Subtitle
908 h2. Subtitle
909
909
910 h2. Subtitle
910 h2. Subtitle
911 RAW
911 RAW
912
912
913 expected = '<ul class="toc">' +
913 expected = '<ul class="toc">' +
914 '<li><a href="#Title">Title</a>' +
914 '<li><a href="#Title">Title</a>' +
915 '<ul>' +
915 '<ul>' +
916 '<li><a href="#Subtitle">Subtitle</a></li>' +
916 '<li><a href="#Subtitle">Subtitle</a></li>' +
917 '<li><a href="#Subtitle-2">Subtitle</a></li>'
917 '<li><a href="#Subtitle-2">Subtitle</a></li>'
918 '</ul>'
918 '</ul>'
919 '</li>' +
919 '</li>' +
920 '</ul>'
920 '</ul>'
921
921
922 @project = Project.find(1)
922 @project = Project.find(1)
923 result = textilizable(raw).gsub("\n", "")
923 result = textilizable(raw).gsub("\n", "")
924 assert_include expected, result
924 assert_include expected, result
925 assert_include '<a name="Subtitle">', result
925 assert_include '<a name="Subtitle">', result
926 assert_include '<a name="Subtitle-2">', result
926 assert_include '<a name="Subtitle-2">', result
927 end
927 end
928
928
929 def test_table_of_content_should_contain_included_page_headings
929 def test_table_of_content_should_contain_included_page_headings
930 raw = <<-RAW
930 raw = <<-RAW
931 {{toc}}
931 {{toc}}
932
932
933 h1. Included
933 h1. Included
934
934
935 {{include(Child_1)}}
935 {{include(Child_1)}}
936 RAW
936 RAW
937
937
938 expected = '<ul class="toc">' +
938 expected = '<ul class="toc">' +
939 '<li><a href="#Included">Included</a></li>' +
939 '<li><a href="#Included">Included</a></li>' +
940 '<li><a href="#Child-page-1">Child page 1</a></li>' +
940 '<li><a href="#Child-page-1">Child page 1</a></li>' +
941 '</ul>'
941 '</ul>'
942
942
943 @project = Project.find(1)
943 @project = Project.find(1)
944 assert textilizable(raw).gsub("\n", "").include?(expected)
944 assert textilizable(raw).gsub("\n", "").include?(expected)
945 end
945 end
946
946
947 def test_section_edit_links
947 def test_section_edit_links
948 raw = <<-RAW
948 raw = <<-RAW
949 h1. Title
949 h1. Title
950
950
951 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
951 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
952
952
953 h2. Subtitle with a [[Wiki]] link
953 h2. Subtitle with a [[Wiki]] link
954
954
955 h2. Subtitle with *some* _modifiers_
955 h2. Subtitle with *some* _modifiers_
956
956
957 h2. Subtitle with @inline code@
957 h2. Subtitle with @inline code@
958
958
959 <pre>
959 <pre>
960 some code
960 some code
961
961
962 h2. heading inside pre
962 h2. heading inside pre
963
963
964 <h2>html heading inside pre</h2>
964 <h2>html heading inside pre</h2>
965 </pre>
965 </pre>
966
966
967 h2. Subtitle after pre tag
967 h2. Subtitle after pre tag
968 RAW
968 RAW
969
969
970 @project = Project.find(1)
970 @project = Project.find(1)
971 set_language_if_valid 'en'
971 set_language_if_valid 'en'
972 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
972 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
973
973
974 # heading that contains inline code
974 # heading that contains inline code
975 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
975 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
976 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
976 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
977 '<a name="Subtitle-with-inline-code"></a>' +
977 '<a name="Subtitle-with-inline-code"></a>' +
978 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
978 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
979 result
979 result
980
980
981 # last heading
981 # last heading
982 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
982 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
983 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
983 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
984 '<a name="Subtitle-after-pre-tag"></a>' +
984 '<a name="Subtitle-after-pre-tag"></a>' +
985 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
985 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
986 result
986 result
987 end
987 end
988
988
989 def test_default_formatter
989 def test_default_formatter
990 with_settings :text_formatting => 'unknown' do
990 with_settings :text_formatting => 'unknown' do
991 text = 'a *link*: http://www.example.net/'
991 text = 'a *link*: http://www.example.net/'
992 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
992 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
993 end
993 end
994 end
994 end
995
995
996 def test_due_date_distance_in_words
996 def test_due_date_distance_in_words
997 to_test = { Date.today => 'Due in 0 days',
997 to_test = { Date.today => 'Due in 0 days',
998 Date.today + 1 => 'Due in 1 day',
998 Date.today + 1 => 'Due in 1 day',
999 Date.today + 100 => 'Due in about 3 months',
999 Date.today + 100 => 'Due in about 3 months',
1000 Date.today + 20000 => 'Due in over 54 years',
1000 Date.today + 20000 => 'Due in over 54 years',
1001 Date.today - 1 => '1 day late',
1001 Date.today - 1 => '1 day late',
1002 Date.today - 100 => 'about 3 months late',
1002 Date.today - 100 => 'about 3 months late',
1003 Date.today - 20000 => 'over 54 years late',
1003 Date.today - 20000 => 'over 54 years late',
1004 }
1004 }
1005 ::I18n.locale = :en
1005 ::I18n.locale = :en
1006 to_test.each do |date, expected|
1006 to_test.each do |date, expected|
1007 assert_equal expected, due_date_distance_in_words(date)
1007 assert_equal expected, due_date_distance_in_words(date)
1008 end
1008 end
1009 end
1009 end
1010
1010
1011 def test_avatar_enabled
1011 def test_avatar_enabled
1012 with_settings :gravatar_enabled => '1' do
1012 with_settings :gravatar_enabled => '1' do
1013 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1013 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1014 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1014 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1015 # Default size is 50
1015 # Default size is 50
1016 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1016 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1017 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1017 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1018 # Non-avatar options should be considered html options
1018 # Non-avatar options should be considered html options
1019 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1019 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1020 # The default class of the img tag should be gravatar
1020 # The default class of the img tag should be gravatar
1021 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1021 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1022 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1022 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1023 assert_nil avatar('jsmith')
1023 assert_nil avatar('jsmith')
1024 assert_nil avatar(nil)
1024 assert_nil avatar(nil)
1025 end
1025 end
1026 end
1026 end
1027
1027
1028 def test_avatar_disabled
1028 def test_avatar_disabled
1029 with_settings :gravatar_enabled => '0' do
1029 with_settings :gravatar_enabled => '0' do
1030 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1030 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1031 end
1031 end
1032 end
1032 end
1033
1033
1034 def test_link_to_user
1034 def test_link_to_user
1035 user = User.find(2)
1035 user = User.find(2)
1036 assert_equal '<a href="/users/2" class="user active">John Smith</a>', link_to_user(user)
1036 assert_equal '<a href="/users/2" class="user active">John Smith</a>', link_to_user(user)
1037 end
1037 end
1038
1038
1039 def test_link_to_user_should_not_link_to_locked_user
1039 def test_link_to_user_should_not_link_to_locked_user
1040 with_current_user nil do
1040 with_current_user nil do
1041 user = User.find(5)
1041 user = User.find(5)
1042 assert user.locked?
1042 assert user.locked?
1043 assert_equal 'Dave2 Lopper2', link_to_user(user)
1043 assert_equal 'Dave2 Lopper2', link_to_user(user)
1044 end
1044 end
1045 end
1045 end
1046
1046
1047 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1047 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1048 with_current_user User.find(1) do
1048 with_current_user User.find(1) do
1049 user = User.find(5)
1049 user = User.find(5)
1050 assert user.locked?
1050 assert user.locked?
1051 assert_equal '<a href="/users/5" class="user locked">Dave2 Lopper2</a>', link_to_user(user)
1051 assert_equal '<a href="/users/5" class="user locked">Dave2 Lopper2</a>', link_to_user(user)
1052 end
1052 end
1053 end
1053 end
1054
1054
1055 def test_link_to_user_should_not_link_to_anonymous
1055 def test_link_to_user_should_not_link_to_anonymous
1056 user = User.anonymous
1056 user = User.anonymous
1057 assert user.anonymous?
1057 assert user.anonymous?
1058 t = link_to_user(user)
1058 t = link_to_user(user)
1059 assert_equal ::I18n.t(:label_user_anonymous), t
1059 assert_equal ::I18n.t(:label_user_anonymous), t
1060 end
1060 end
1061
1061
1062 def test_link_to_project
1062 def test_link_to_project
1063 project = Project.find(1)
1063 project = Project.find(1)
1064 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1064 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1065 link_to_project(project)
1065 link_to_project(project)
1066 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1066 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
1067 link_to_project(project, :action => 'settings')
1067 link_to_project(project, :action => 'settings')
1068 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1068 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1069 link_to_project(project, {:only_path => false, :jump => 'blah'})
1069 link_to_project(project, {:only_path => false, :jump => 'blah'})
1070 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1070 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
1071 link_to_project(project, {:action => 'settings'}, :class => "project")
1071 link_to_project(project, {:action => 'settings'}, :class => "project")
1072 end
1072 end
1073
1073
1074 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1074 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1075 # numeric identifier are no longer allowed
1075 # numeric identifier are no longer allowed
1076 Project.update_all "identifier=25", "id=1"
1076 Project.update_all "identifier=25", "id=1"
1077
1077
1078 assert_equal '<a href="/projects/1">eCookbook</a>',
1078 assert_equal '<a href="/projects/1">eCookbook</a>',
1079 link_to_project(Project.find(1))
1079 link_to_project(Project.find(1))
1080 end
1080 end
1081
1081
1082 def test_principals_options_for_select_with_users
1082 def test_principals_options_for_select_with_users
1083 User.current = nil
1083 User.current = nil
1084 users = [User.find(2), User.find(4)]
1084 users = [User.find(2), User.find(4)]
1085 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1085 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1086 principals_options_for_select(users)
1086 principals_options_for_select(users)
1087 end
1087 end
1088
1088
1089 def test_principals_options_for_select_with_selected
1089 def test_principals_options_for_select_with_selected
1090 User.current = nil
1090 User.current = nil
1091 users = [User.find(2), User.find(4)]
1091 users = [User.find(2), User.find(4)]
1092 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1092 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1093 principals_options_for_select(users, User.find(4))
1093 principals_options_for_select(users, User.find(4))
1094 end
1094 end
1095
1095
1096 def test_principals_options_for_select_with_users_and_groups
1096 def test_principals_options_for_select_with_users_and_groups
1097 User.current = nil
1097 User.current = nil
1098 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1098 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1099 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1099 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1100 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1100 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1101 principals_options_for_select(users)
1101 principals_options_for_select(users)
1102 end
1102 end
1103
1103
1104 def test_principals_options_for_select_with_empty_collection
1104 def test_principals_options_for_select_with_empty_collection
1105 assert_equal '', principals_options_for_select([])
1105 assert_equal '', principals_options_for_select([])
1106 end
1106 end
1107
1107
1108 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1108 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1109 users = [User.find(2), User.find(4)]
1109 users = [User.find(2), User.find(4)]
1110 User.current = User.find(4)
1110 User.current = User.find(4)
1111 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1111 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1112 end
1112 end
1113
1113
1114 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1114 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1115 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1115 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1116 end
1116 end
1117
1117
1118 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1118 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1119 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1119 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1120 end
1120 end
1121
1121
1122 def test_image_tag_should_pick_the_default_image
1122 def test_image_tag_should_pick_the_default_image
1123 assert_match 'src="/images/image.png"', image_tag("image.png")
1123 assert_match 'src="/images/image.png"', image_tag("image.png")
1124 end
1124 end
1125
1125
1126 def test_image_tag_should_pick_the_theme_image_if_it_exists
1126 def test_image_tag_should_pick_the_theme_image_if_it_exists
1127 theme = Redmine::Themes.themes.last
1127 theme = Redmine::Themes.themes.last
1128 theme.images << 'image.png'
1128 theme.images << 'image.png'
1129
1129
1130 with_settings :ui_theme => theme.id do
1130 with_settings :ui_theme => theme.id do
1131 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1131 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1132 assert_match %|src="/images/other.png"|, image_tag("other.png")
1132 assert_match %|src="/images/other.png"|, image_tag("other.png")
1133 end
1133 end
1134 ensure
1134 ensure
1135 theme.images.delete 'image.png'
1135 theme.images.delete 'image.png'
1136 end
1136 end
1137
1137
1138 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1138 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1139 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1139 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1140 end
1140 end
1141
1141
1142 def test_javascript_include_tag_should_pick_the_default_javascript
1142 def test_javascript_include_tag_should_pick_the_default_javascript
1143 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1143 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1144 end
1144 end
1145
1145
1146 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1146 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1147 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1147 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1148 end
1148 end
1149
1149
1150 def test_per_page_links_should_show_usefull_values
1150 def test_per_page_links_should_show_usefull_values
1151 set_language_if_valid 'en'
1151 set_language_if_valid 'en'
1152 stubs(:link_to).returns("[link]")
1152 stubs(:link_to).returns("[link]")
1153
1153
1154 with_settings :per_page_options => '10, 25, 50, 100' do
1154 with_settings :per_page_options => '10, 25, 50, 100' do
1155 assert_nil per_page_links(10, 3)
1155 assert_nil per_page_links(10, 3)
1156 assert_nil per_page_links(25, 3)
1156 assert_nil per_page_links(25, 3)
1157 assert_equal "Per page: 10, [link]", per_page_links(10, 22)
1157 assert_equal "Per page: 10, [link]", per_page_links(10, 22)
1158 assert_equal "Per page: [link], 25", per_page_links(25, 22)
1158 assert_equal "Per page: [link], 25", per_page_links(25, 22)
1159 assert_equal "Per page: [link], [link], 50", per_page_links(50, 22)
1159 assert_equal "Per page: [link], [link], 50", per_page_links(50, 22)
1160 assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26)
1160 assert_equal "Per page: [link], 25, [link]", per_page_links(25, 26)
1161 assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120)
1161 assert_equal "Per page: [link], 25, [link], [link]", per_page_links(25, 120)
1162 end
1162 end
1163 end
1163 end
1164 end
1164 end
@@ -1,377 +1,377
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class IssueNestedSetTest < ActiveSupport::TestCase
20 class IssueNestedSetTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 def test_create_root_issue
30 def test_create_root_issue
31 issue1 = Issue.generate!
31 issue1 = Issue.generate!
32 issue2 = Issue.generate!
32 issue2 = Issue.generate!
33 issue1.reload
33 issue1.reload
34 issue2.reload
34 issue2.reload
35
35
36 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
36 assert_equal [issue1.id, nil, 1, 2], [issue1.root_id, issue1.parent_id, issue1.lft, issue1.rgt]
37 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
37 assert_equal [issue2.id, nil, 1, 2], [issue2.root_id, issue2.parent_id, issue2.lft, issue2.rgt]
38 end
38 end
39
39
40 def test_create_child_issue
40 def test_create_child_issue
41 parent = Issue.generate!
41 parent = Issue.generate!
42 child = Issue.generate!(:parent_issue_id => parent.id)
42 child = Issue.generate!(:parent_issue_id => parent.id)
43 parent.reload
43 parent.reload
44 child.reload
44 child.reload
45
45
46 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
46 assert_equal [parent.id, nil, 1, 4], [parent.root_id, parent.parent_id, parent.lft, parent.rgt]
47 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
47 assert_equal [parent.id, parent.id, 2, 3], [child.root_id, child.parent_id, child.lft, child.rgt]
48 end
48 end
49
49
50 def test_creating_a_child_in_a_subproject_should_validate
50 def test_creating_a_child_in_a_subproject_should_validate
51 issue = Issue.generate!
51 issue = Issue.generate!
52 child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1,
52 child = Issue.new(:project_id => 3, :tracker_id => 2, :author_id => 1,
53 :subject => 'child', :parent_issue_id => issue.id)
53 :subject => 'child', :parent_issue_id => issue.id)
54 assert_save child
54 assert_save child
55 assert_equal issue, child.reload.parent
55 assert_equal issue, child.reload.parent
56 end
56 end
57
57
58 def test_creating_a_child_in_an_invalid_project_should_not_validate
58 def test_creating_a_child_in_an_invalid_project_should_not_validate
59 issue = Issue.generate!
59 issue = Issue.generate!
60 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
60 child = Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
61 :subject => 'child', :parent_issue_id => issue.id)
61 :subject => 'child', :parent_issue_id => issue.id)
62 assert !child.save
62 assert !child.save
63 assert_not_nil child.errors[:parent_issue_id]
63 assert_not_nil child.errors[:parent_issue_id]
64 end
64 end
65
65
66 def test_move_a_root_to_child
66 def test_move_a_root_to_child
67 parent1 = Issue.generate!
67 parent1 = Issue.generate!
68 parent2 = Issue.generate!
68 parent2 = Issue.generate!
69 child = Issue.generate!(:parent_issue_id => parent1.id)
69 child = Issue.generate!(:parent_issue_id => parent1.id)
70
70
71 parent2.parent_issue_id = parent1.id
71 parent2.parent_issue_id = parent1.id
72 parent2.save!
72 parent2.save!
73 child.reload
73 child.reload
74 parent1.reload
74 parent1.reload
75 parent2.reload
75 parent2.reload
76
76
77 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
77 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
78 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
78 assert_equal [parent1.id, 4, 5], [parent2.root_id, parent2.lft, parent2.rgt]
79 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
79 assert_equal [parent1.id, 2, 3], [child.root_id, child.lft, child.rgt]
80 end
80 end
81
81
82 def test_move_a_child_to_root
82 def test_move_a_child_to_root
83 parent1 = Issue.generate!
83 parent1 = Issue.generate!
84 parent2 = Issue.generate!
84 parent2 = Issue.generate!
85 child = Issue.generate!(:parent_issue_id => parent1.id)
85 child = Issue.generate!(:parent_issue_id => parent1.id)
86
86
87 child.parent_issue_id = nil
87 child.parent_issue_id = nil
88 child.save!
88 child.save!
89 child.reload
89 child.reload
90 parent1.reload
90 parent1.reload
91 parent2.reload
91 parent2.reload
92
92
93 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
93 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
94 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
94 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
95 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
95 assert_equal [child.id, 1, 2], [child.root_id, child.lft, child.rgt]
96 end
96 end
97
97
98 def test_move_a_child_to_another_issue
98 def test_move_a_child_to_another_issue
99 parent1 = Issue.generate!
99 parent1 = Issue.generate!
100 parent2 = Issue.generate!
100 parent2 = Issue.generate!
101 child = Issue.generate!(:parent_issue_id => parent1.id)
101 child = Issue.generate!(:parent_issue_id => parent1.id)
102
102
103 child.parent_issue_id = parent2.id
103 child.parent_issue_id = parent2.id
104 child.save!
104 child.save!
105 child.reload
105 child.reload
106 parent1.reload
106 parent1.reload
107 parent2.reload
107 parent2.reload
108
108
109 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
109 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
110 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
110 assert_equal [parent2.id, 1, 4], [parent2.root_id, parent2.lft, parent2.rgt]
111 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
111 assert_equal [parent2.id, 2, 3], [child.root_id, child.lft, child.rgt]
112 end
112 end
113
113
114 def test_move_a_child_with_descendants_to_another_issue
114 def test_move_a_child_with_descendants_to_another_issue
115 parent1 = Issue.generate!
115 parent1 = Issue.generate!
116 parent2 = Issue.generate!
116 parent2 = Issue.generate!
117 child = Issue.generate!(:parent_issue_id => parent1.id)
117 child = Issue.generate!(:parent_issue_id => parent1.id)
118 grandchild = Issue.generate!(:parent_issue_id => child.id)
118 grandchild = Issue.generate!(:parent_issue_id => child.id)
119
119
120 parent1.reload
120 parent1.reload
121 parent2.reload
121 parent2.reload
122 child.reload
122 child.reload
123 grandchild.reload
123 grandchild.reload
124
124
125 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
125 assert_equal [parent1.id, 1, 6], [parent1.root_id, parent1.lft, parent1.rgt]
126 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
126 assert_equal [parent2.id, 1, 2], [parent2.root_id, parent2.lft, parent2.rgt]
127 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
127 assert_equal [parent1.id, 2, 5], [child.root_id, child.lft, child.rgt]
128 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
128 assert_equal [parent1.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
129
129
130 child.reload.parent_issue_id = parent2.id
130 child.reload.parent_issue_id = parent2.id
131 child.save!
131 child.save!
132 child.reload
132 child.reload
133 grandchild.reload
133 grandchild.reload
134 parent1.reload
134 parent1.reload
135 parent2.reload
135 parent2.reload
136
136
137 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
137 assert_equal [parent1.id, 1, 2], [parent1.root_id, parent1.lft, parent1.rgt]
138 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
138 assert_equal [parent2.id, 1, 6], [parent2.root_id, parent2.lft, parent2.rgt]
139 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
139 assert_equal [parent2.id, 2, 5], [child.root_id, child.lft, child.rgt]
140 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
140 assert_equal [parent2.id, 3, 4], [grandchild.root_id, grandchild.lft, grandchild.rgt]
141 end
141 end
142
142
143 def test_move_a_child_with_descendants_to_another_project
143 def test_move_a_child_with_descendants_to_another_project
144 parent1 = Issue.generate!
144 parent1 = Issue.generate!
145 child = Issue.generate!(:parent_issue_id => parent1.id)
145 child = Issue.generate!(:parent_issue_id => parent1.id)
146 grandchild = Issue.generate!(:parent_issue_id => child.id)
146 grandchild = Issue.generate!(:parent_issue_id => child.id)
147
147
148 child.reload
148 child.reload
149 child.project = Project.find(2)
149 child.project = Project.find(2)
150 assert child.save
150 assert child.save
151 child.reload
151 child.reload
152 grandchild.reload
152 grandchild.reload
153 parent1.reload
153 parent1.reload
154
154
155 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
155 assert_equal [1, parent1.id, 1, 2], [parent1.project_id, parent1.root_id, parent1.lft, parent1.rgt]
156 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
156 assert_equal [2, child.id, 1, 4], [child.project_id, child.root_id, child.lft, child.rgt]
157 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
157 assert_equal [2, child.id, 2, 3], [grandchild.project_id, grandchild.root_id, grandchild.lft, grandchild.rgt]
158 end
158 end
159
159
160 def test_moving_an_issue_to_a_descendant_should_not_validate
160 def test_moving_an_issue_to_a_descendant_should_not_validate
161 parent1 = Issue.generate!
161 parent1 = Issue.generate!
162 parent2 = Issue.generate!
162 parent2 = Issue.generate!
163 child = Issue.generate!(:parent_issue_id => parent1.id)
163 child = Issue.generate!(:parent_issue_id => parent1.id)
164 grandchild = Issue.generate!(:parent_issue_id => child.id)
164 grandchild = Issue.generate!(:parent_issue_id => child.id)
165
165
166 child.reload
166 child.reload
167 child.parent_issue_id = grandchild.id
167 child.parent_issue_id = grandchild.id
168 assert !child.save
168 assert !child.save
169 assert_not_nil child.errors[:parent_issue_id]
169 assert_not_nil child.errors[:parent_issue_id]
170 end
170 end
171
171
172 def test_moving_an_issue_should_keep_valid_relations_only
172 def test_moving_an_issue_should_keep_valid_relations_only
173 issue1 = Issue.generate!
173 issue1 = Issue.generate!
174 issue2 = Issue.generate!
174 issue2 = Issue.generate!
175 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
175 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
176 issue4 = Issue.generate!
176 issue4 = Issue.generate!
177 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
177 r1 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
178 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
178 r2 = IssueRelation.create!(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_PRECEDES)
179 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
179 r3 = IssueRelation.create!(:issue_from => issue2, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
180 issue2.reload
180 issue2.reload
181 issue2.parent_issue_id = issue1.id
181 issue2.parent_issue_id = issue1.id
182 issue2.save!
182 issue2.save!
183 assert !IssueRelation.exists?(r1.id)
183 assert !IssueRelation.exists?(r1.id)
184 assert !IssueRelation.exists?(r2.id)
184 assert !IssueRelation.exists?(r2.id)
185 assert IssueRelation.exists?(r3.id)
185 assert IssueRelation.exists?(r3.id)
186 end
186 end
187
187
188 def test_destroy_should_destroy_children
188 def test_destroy_should_destroy_children
189 issue1 = Issue.generate!
189 issue1 = Issue.generate!
190 issue2 = Issue.generate!
190 issue2 = Issue.generate!
191 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
191 issue3 = Issue.generate!(:parent_issue_id => issue2.id)
192 issue4 = Issue.generate!(:parent_issue_id => issue1.id)
192 issue4 = Issue.generate!(:parent_issue_id => issue1.id)
193
193
194 issue3.init_journal(User.find(2))
194 issue3.init_journal(User.find(2))
195 issue3.subject = 'child with journal'
195 issue3.subject = 'child with journal'
196 issue3.save!
196 issue3.save!
197
197
198 assert_difference 'Issue.count', -2 do
198 assert_difference 'Issue.count', -2 do
199 assert_difference 'Journal.count', -1 do
199 assert_difference 'Journal.count', -1 do
200 assert_difference 'JournalDetail.count', -1 do
200 assert_difference 'JournalDetail.count', -1 do
201 Issue.find(issue2.id).destroy
201 Issue.find(issue2.id).destroy
202 end
202 end
203 end
203 end
204 end
204 end
205
205
206 issue1.reload
206 issue1.reload
207 issue4.reload
207 issue4.reload
208 assert !Issue.exists?(issue2.id)
208 assert !Issue.exists?(issue2.id)
209 assert !Issue.exists?(issue3.id)
209 assert !Issue.exists?(issue3.id)
210 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
210 assert_equal [issue1.id, 1, 4], [issue1.root_id, issue1.lft, issue1.rgt]
211 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
211 assert_equal [issue1.id, 2, 3], [issue4.root_id, issue4.lft, issue4.rgt]
212 end
212 end
213
213
214 def test_destroy_child_should_update_parent
214 def test_destroy_child_should_update_parent
215 issue = Issue.generate!
215 issue = Issue.generate!
216 child1 = Issue.generate!(:parent_issue_id => issue.id)
216 child1 = Issue.generate!(:parent_issue_id => issue.id)
217 child2 = Issue.generate!(:parent_issue_id => issue.id)
217 child2 = Issue.generate!(:parent_issue_id => issue.id)
218
218
219 issue.reload
219 issue.reload
220 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
220 assert_equal [issue.id, 1, 6], [issue.root_id, issue.lft, issue.rgt]
221
221
222 child2.reload.destroy
222 child2.reload.destroy
223
223
224 issue.reload
224 issue.reload
225 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
225 assert_equal [issue.id, 1, 4], [issue.root_id, issue.lft, issue.rgt]
226 end
226 end
227
227
228 def test_destroy_parent_issue_updated_during_children_destroy
228 def test_destroy_parent_issue_updated_during_children_destroy
229 parent = Issue.generate!
229 parent = Issue.generate!
230 Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id)
230 Issue.generate!(:start_date => Date.today, :parent_issue_id => parent.id)
231 Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
231 Issue.generate!(:start_date => 2.days.from_now, :parent_issue_id => parent.id)
232
232
233 assert_difference 'Issue.count', -3 do
233 assert_difference 'Issue.count', -3 do
234 Issue.find(parent.id).destroy
234 Issue.find(parent.id).destroy
235 end
235 end
236 end
236 end
237
237
238 def test_destroy_child_issue_with_children
238 def test_destroy_child_issue_with_children
239 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
239 root = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'root')
240 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
240 child = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'child', :parent_issue_id => root.id)
241 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
241 leaf = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'leaf', :parent_issue_id => child.id)
242 leaf.init_journal(User.find(2))
242 leaf.init_journal(User.find(2))
243 leaf.subject = 'leaf with journal'
243 leaf.subject = 'leaf with journal'
244 leaf.save!
244 leaf.save!
245
245
246 assert_difference 'Issue.count', -2 do
246 assert_difference 'Issue.count', -2 do
247 assert_difference 'Journal.count', -1 do
247 assert_difference 'Journal.count', -1 do
248 assert_difference 'JournalDetail.count', -1 do
248 assert_difference 'JournalDetail.count', -1 do
249 Issue.find(child.id).destroy
249 Issue.find(child.id).destroy
250 end
250 end
251 end
251 end
252 end
252 end
253
253
254 root = Issue.find(root.id)
254 root = Issue.find(root.id)
255 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
255 assert root.leaf?, "Root issue is not a leaf (lft: #{root.lft}, rgt: #{root.rgt})"
256 end
256 end
257
257
258 def test_destroy_issue_with_grand_child
258 def test_destroy_issue_with_grand_child
259 parent = Issue.generate!
259 parent = Issue.generate!
260 issue = Issue.generate!(:parent_issue_id => parent.id)
260 issue = Issue.generate!(:parent_issue_id => parent.id)
261 child = Issue.generate!(:parent_issue_id => issue.id)
261 child = Issue.generate!(:parent_issue_id => issue.id)
262 grandchild1 = Issue.generate!(:parent_issue_id => child.id)
262 grandchild1 = Issue.generate!(:parent_issue_id => child.id)
263 grandchild2 = Issue.generate!(:parent_issue_id => child.id)
263 grandchild2 = Issue.generate!(:parent_issue_id => child.id)
264
264
265 assert_difference 'Issue.count', -4 do
265 assert_difference 'Issue.count', -4 do
266 Issue.find(issue.id).destroy
266 Issue.find(issue.id).destroy
267 parent.reload
267 parent.reload
268 assert_equal [1, 2], [parent.lft, parent.rgt]
268 assert_equal [1, 2], [parent.lft, parent.rgt]
269 end
269 end
270 end
270 end
271
271
272 def test_parent_priority_should_be_the_highest_child_priority
272 def test_parent_priority_should_be_the_highest_child_priority
273 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
273 parent = Issue.generate!(:priority => IssuePriority.find_by_name('Normal'))
274 # Create children
274 # Create children
275 child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
275 child1 = Issue.generate!(:priority => IssuePriority.find_by_name('High'), :parent_issue_id => parent.id)
276 assert_equal 'High', parent.reload.priority.name
276 assert_equal 'High', parent.reload.priority.name
277 child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
277 child2 = Issue.generate!(:priority => IssuePriority.find_by_name('Immediate'), :parent_issue_id => child1.id)
278 assert_equal 'Immediate', child1.reload.priority.name
278 assert_equal 'Immediate', child1.reload.priority.name
279 assert_equal 'Immediate', parent.reload.priority.name
279 assert_equal 'Immediate', parent.reload.priority.name
280 child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
280 child3 = Issue.generate!(:priority => IssuePriority.find_by_name('Low'), :parent_issue_id => parent.id)
281 assert_equal 'Immediate', parent.reload.priority.name
281 assert_equal 'Immediate', parent.reload.priority.name
282 # Destroy a child
282 # Destroy a child
283 child1.destroy
283 child1.destroy
284 assert_equal 'Low', parent.reload.priority.name
284 assert_equal 'Low', parent.reload.priority.name
285 # Update a child
285 # Update a child
286 child3.reload.priority = IssuePriority.find_by_name('Normal')
286 child3.reload.priority = IssuePriority.find_by_name('Normal')
287 child3.save!
287 child3.save!
288 assert_equal 'Normal', parent.reload.priority.name
288 assert_equal 'Normal', parent.reload.priority.name
289 end
289 end
290
290
291 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
291 def test_parent_dates_should_be_lowest_start_and_highest_due_dates
292 parent = Issue.generate!
292 parent = Issue.generate!
293 Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
293 Issue.generate!(:start_date => '2010-01-25', :due_date => '2010-02-15', :parent_issue_id => parent.id)
294 Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
294 Issue.generate!( :due_date => '2010-02-13', :parent_issue_id => parent.id)
295 Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
295 Issue.generate!(:start_date => '2010-02-01', :due_date => '2010-02-22', :parent_issue_id => parent.id)
296 parent.reload
296 parent.reload
297 assert_equal Date.parse('2010-01-25'), parent.start_date
297 assert_equal Date.parse('2010-01-25'), parent.start_date
298 assert_equal Date.parse('2010-02-22'), parent.due_date
298 assert_equal Date.parse('2010-02-22'), parent.due_date
299 end
299 end
300
300
301 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
301 def test_parent_done_ratio_should_be_average_done_ratio_of_leaves
302 parent = Issue.generate!
302 parent = Issue.generate!
303 Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id)
303 Issue.generate!(:done_ratio => 20, :parent_issue_id => parent.id)
304 assert_equal 20, parent.reload.done_ratio
304 assert_equal 20, parent.reload.done_ratio
305 Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id)
305 Issue.generate!(:done_ratio => 70, :parent_issue_id => parent.id)
306 assert_equal 45, parent.reload.done_ratio
306 assert_equal 45, parent.reload.done_ratio
307
307
308 child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id)
308 child = Issue.generate!(:done_ratio => 0, :parent_issue_id => parent.id)
309 assert_equal 30, parent.reload.done_ratio
309 assert_equal 30, parent.reload.done_ratio
310
310
311 Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id)
311 Issue.generate!(:done_ratio => 30, :parent_issue_id => child.id)
312 assert_equal 30, child.reload.done_ratio
312 assert_equal 30, child.reload.done_ratio
313 assert_equal 40, parent.reload.done_ratio
313 assert_equal 40, parent.reload.done_ratio
314 end
314 end
315
315
316 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
316 def test_parent_done_ratio_should_be_weighted_by_estimated_times_if_any
317 parent = Issue.generate!
317 parent = Issue.generate!
318 Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
318 Issue.generate!(:estimated_hours => 10, :done_ratio => 20, :parent_issue_id => parent.id)
319 assert_equal 20, parent.reload.done_ratio
319 assert_equal 20, parent.reload.done_ratio
320 Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
320 Issue.generate!(:estimated_hours => 20, :done_ratio => 50, :parent_issue_id => parent.id)
321 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
321 assert_equal (50 * 20 + 20 * 10) / 30, parent.reload.done_ratio
322 end
322 end
323
323
324 def test_parent_estimate_should_be_sum_of_leaves
324 def test_parent_estimate_should_be_sum_of_leaves
325 parent = Issue.generate!
325 parent = Issue.generate!
326 Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id)
326 Issue.generate!(:estimated_hours => nil, :parent_issue_id => parent.id)
327 assert_equal nil, parent.reload.estimated_hours
327 assert_equal nil, parent.reload.estimated_hours
328 Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id)
328 Issue.generate!(:estimated_hours => 5, :parent_issue_id => parent.id)
329 assert_equal 5, parent.reload.estimated_hours
329 assert_equal 5, parent.reload.estimated_hours
330 Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id)
330 Issue.generate!(:estimated_hours => 7, :parent_issue_id => parent.id)
331 assert_equal 12, parent.reload.estimated_hours
331 assert_equal 12, parent.reload.estimated_hours
332 end
332 end
333
333
334 def test_move_parent_updates_old_parent_attributes
334 def test_move_parent_updates_old_parent_attributes
335 first_parent = Issue.generate!
335 first_parent = Issue.generate!
336 second_parent = Issue.generate!
336 second_parent = Issue.generate!
337 child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
337 child = Issue.generate!(:estimated_hours => 5, :parent_issue_id => first_parent.id)
338 assert_equal 5, first_parent.reload.estimated_hours
338 assert_equal 5, first_parent.reload.estimated_hours
339 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
339 child.update_attributes(:estimated_hours => 7, :parent_issue_id => second_parent.id)
340 assert_equal 7, second_parent.reload.estimated_hours
340 assert_equal 7, second_parent.reload.estimated_hours
341 assert_nil first_parent.reload.estimated_hours
341 assert_nil first_parent.reload.estimated_hours
342 end
342 end
343
343
344 def test_reschuling_a_parent_should_reschedule_subtasks
344 def test_reschuling_a_parent_should_reschedule_subtasks
345 parent = Issue.generate!
345 parent = Issue.generate!
346 c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
346 c1 = Issue.generate!(:start_date => '2010-05-12', :due_date => '2010-05-18', :parent_issue_id => parent.id)
347 c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
347 c2 = Issue.generate!(:start_date => '2010-06-03', :due_date => '2010-06-10', :parent_issue_id => parent.id)
348 parent.reload
348 parent.reload
349 parent.reschedule_on!(Date.parse('2010-06-02'))
349 parent.reschedule_on!(Date.parse('2010-06-02'))
350 c1.reload
350 c1.reload
351 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
351 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-08')], [c1.start_date, c1.due_date]
352 c2.reload
352 c2.reload
353 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
353 assert_equal [Date.parse('2010-06-03'), Date.parse('2010-06-10')], [c2.start_date, c2.due_date] # no change
354 parent.reload
354 parent.reload
355 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
355 assert_equal [Date.parse('2010-06-02'), Date.parse('2010-06-10')], [parent.start_date, parent.due_date]
356 end
356 end
357
357
358 def test_project_copy_should_copy_issue_tree
358 def test_project_copy_should_copy_issue_tree
359 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
359 p = Project.create!(:name => 'Tree copy', :identifier => 'tree-copy', :tracker_ids => [1, 2])
360 i1 = Issue.generate!(:project => p, :subject => 'i1')
360 i1 = Issue.generate!(:project => p, :subject => 'i1')
361 i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id)
361 i2 = Issue.generate!(:project => p, :subject => 'i2', :parent_issue_id => i1.id)
362 i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id)
362 i3 = Issue.generate!(:project => p, :subject => 'i3', :parent_issue_id => i1.id)
363 i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id)
363 i4 = Issue.generate!(:project => p, :subject => 'i4', :parent_issue_id => i2.id)
364 i5 = Issue.generate!(:project => p, :subject => 'i5')
364 i5 = Issue.generate!(:project => p, :subject => 'i5')
365 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
365 c = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1, 2])
366 c.copy(p, :only => 'issues')
366 c.copy(p, :only => 'issues')
367 c.reload
367 c.reload
368
368
369 assert_equal 5, c.issues.count
369 assert_equal 5, c.issues.count
370 ic1, ic2, ic3, ic4, ic5 = c.issues.find(:all, :order => 'subject')
370 ic1, ic2, ic3, ic4, ic5 = c.issues.order('subject').all
371 assert ic1.root?
371 assert ic1.root?
372 assert_equal ic1, ic2.parent
372 assert_equal ic1, ic2.parent
373 assert_equal ic1, ic3.parent
373 assert_equal ic1, ic3.parent
374 assert_equal ic2, ic4.parent
374 assert_equal ic2, ic4.parent
375 assert ic5.root?
375 assert ic5.root?
376 end
376 end
377 end
377 end
@@ -1,1212 +1,1212
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :custom_fields,
25 :custom_fields,
26 :custom_fields_projects,
26 :custom_fields_projects,
27 :custom_fields_trackers,
27 :custom_fields_trackers,
28 :custom_values,
28 :custom_values,
29 :roles,
29 :roles,
30 :member_roles,
30 :member_roles,
31 :members,
31 :members,
32 :enabled_modules,
32 :enabled_modules,
33 :workflows,
33 :workflows,
34 :versions,
34 :versions,
35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
36 :groups_users,
36 :groups_users,
37 :boards, :messages,
37 :boards, :messages,
38 :repositories,
38 :repositories,
39 :news, :comments,
39 :news, :comments,
40 :documents
40 :documents
41
41
42 def setup
42 def setup
43 @ecookbook = Project.find(1)
43 @ecookbook = Project.find(1)
44 @ecookbook_sub1 = Project.find(3)
44 @ecookbook_sub1 = Project.find(3)
45 set_tmp_attachments_directory
45 set_tmp_attachments_directory
46 User.current = nil
46 User.current = nil
47 end
47 end
48
48
49 def test_truth
49 def test_truth
50 assert_kind_of Project, @ecookbook
50 assert_kind_of Project, @ecookbook
51 assert_equal "eCookbook", @ecookbook.name
51 assert_equal "eCookbook", @ecookbook.name
52 end
52 end
53
53
54 def test_default_attributes
54 def test_default_attributes
55 with_settings :default_projects_public => '1' do
55 with_settings :default_projects_public => '1' do
56 assert_equal true, Project.new.is_public
56 assert_equal true, Project.new.is_public
57 assert_equal false, Project.new(:is_public => false).is_public
57 assert_equal false, Project.new(:is_public => false).is_public
58 end
58 end
59
59
60 with_settings :default_projects_public => '0' do
60 with_settings :default_projects_public => '0' do
61 assert_equal false, Project.new.is_public
61 assert_equal false, Project.new.is_public
62 assert_equal true, Project.new(:is_public => true).is_public
62 assert_equal true, Project.new(:is_public => true).is_public
63 end
63 end
64
64
65 with_settings :sequential_project_identifiers => '1' do
65 with_settings :sequential_project_identifiers => '1' do
66 assert !Project.new.identifier.blank?
66 assert !Project.new.identifier.blank?
67 assert Project.new(:identifier => '').identifier.blank?
67 assert Project.new(:identifier => '').identifier.blank?
68 end
68 end
69
69
70 with_settings :sequential_project_identifiers => '0' do
70 with_settings :sequential_project_identifiers => '0' do
71 assert Project.new.identifier.blank?
71 assert Project.new.identifier.blank?
72 assert !Project.new(:identifier => 'test').blank?
72 assert !Project.new(:identifier => 'test').blank?
73 end
73 end
74
74
75 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
75 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
76 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
76 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
77 end
77 end
78
78
79 assert_equal Tracker.all.sort, Project.new.trackers.sort
79 assert_equal Tracker.all.sort, Project.new.trackers.sort
80 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
80 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
81 end
81 end
82
82
83 def test_update
83 def test_update
84 assert_equal "eCookbook", @ecookbook.name
84 assert_equal "eCookbook", @ecookbook.name
85 @ecookbook.name = "eCook"
85 @ecookbook.name = "eCook"
86 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
86 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
87 @ecookbook.reload
87 @ecookbook.reload
88 assert_equal "eCook", @ecookbook.name
88 assert_equal "eCook", @ecookbook.name
89 end
89 end
90
90
91 def test_validate_identifier
91 def test_validate_identifier
92 to_test = {"abc" => true,
92 to_test = {"abc" => true,
93 "ab12" => true,
93 "ab12" => true,
94 "ab-12" => true,
94 "ab-12" => true,
95 "ab_12" => true,
95 "ab_12" => true,
96 "12" => false,
96 "12" => false,
97 "new" => false}
97 "new" => false}
98
98
99 to_test.each do |identifier, valid|
99 to_test.each do |identifier, valid|
100 p = Project.new
100 p = Project.new
101 p.identifier = identifier
101 p.identifier = identifier
102 p.valid?
102 p.valid?
103 if valid
103 if valid
104 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
104 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
105 else
105 else
106 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
106 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
107 end
107 end
108 end
108 end
109 end
109 end
110
110
111 def test_identifier_should_not_be_frozen_for_a_new_project
111 def test_identifier_should_not_be_frozen_for_a_new_project
112 assert_equal false, Project.new.identifier_frozen?
112 assert_equal false, Project.new.identifier_frozen?
113 end
113 end
114
114
115 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
115 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
116 Project.update_all(["identifier = ''"], "id = 1")
116 Project.update_all(["identifier = ''"], "id = 1")
117
117
118 assert_equal false, Project.find(1).identifier_frozen?
118 assert_equal false, Project.find(1).identifier_frozen?
119 end
119 end
120
120
121 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
121 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
122 assert_equal true, Project.find(1).identifier_frozen?
122 assert_equal true, Project.find(1).identifier_frozen?
123 end
123 end
124
124
125 def test_members_should_be_active_users
125 def test_members_should_be_active_users
126 Project.all.each do |project|
126 Project.all.each do |project|
127 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
127 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
128 end
128 end
129 end
129 end
130
130
131 def test_users_should_be_active_users
131 def test_users_should_be_active_users
132 Project.all.each do |project|
132 Project.all.each do |project|
133 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
133 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
134 end
134 end
135 end
135 end
136
136
137 def test_open_scope_on_issues_association
137 def test_open_scope_on_issues_association
138 assert_kind_of Issue, Project.find(1).issues.open.first
138 assert_kind_of Issue, Project.find(1).issues.open.first
139 end
139 end
140
140
141 def test_archive
141 def test_archive
142 user = @ecookbook.members.first.user
142 user = @ecookbook.members.first.user
143 @ecookbook.archive
143 @ecookbook.archive
144 @ecookbook.reload
144 @ecookbook.reload
145
145
146 assert !@ecookbook.active?
146 assert !@ecookbook.active?
147 assert @ecookbook.archived?
147 assert @ecookbook.archived?
148 assert !user.projects.include?(@ecookbook)
148 assert !user.projects.include?(@ecookbook)
149 # Subproject are also archived
149 # Subproject are also archived
150 assert !@ecookbook.children.empty?
150 assert !@ecookbook.children.empty?
151 assert @ecookbook.descendants.active.empty?
151 assert @ecookbook.descendants.active.empty?
152 end
152 end
153
153
154 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
154 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
155 # Assign an issue of a project to a version of a child project
155 # Assign an issue of a project to a version of a child project
156 Issue.find(4).update_attribute :fixed_version_id, 4
156 Issue.find(4).update_attribute :fixed_version_id, 4
157
157
158 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
158 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
159 assert_equal false, @ecookbook.archive
159 assert_equal false, @ecookbook.archive
160 end
160 end
161 @ecookbook.reload
161 @ecookbook.reload
162 assert @ecookbook.active?
162 assert @ecookbook.active?
163 end
163 end
164
164
165 def test_unarchive
165 def test_unarchive
166 user = @ecookbook.members.first.user
166 user = @ecookbook.members.first.user
167 @ecookbook.archive
167 @ecookbook.archive
168 # A subproject of an archived project can not be unarchived
168 # A subproject of an archived project can not be unarchived
169 assert !@ecookbook_sub1.unarchive
169 assert !@ecookbook_sub1.unarchive
170
170
171 # Unarchive project
171 # Unarchive project
172 assert @ecookbook.unarchive
172 assert @ecookbook.unarchive
173 @ecookbook.reload
173 @ecookbook.reload
174 assert @ecookbook.active?
174 assert @ecookbook.active?
175 assert !@ecookbook.archived?
175 assert !@ecookbook.archived?
176 assert user.projects.include?(@ecookbook)
176 assert user.projects.include?(@ecookbook)
177 # Subproject can now be unarchived
177 # Subproject can now be unarchived
178 @ecookbook_sub1.reload
178 @ecookbook_sub1.reload
179 assert @ecookbook_sub1.unarchive
179 assert @ecookbook_sub1.unarchive
180 end
180 end
181
181
182 def test_destroy
182 def test_destroy
183 # 2 active members
183 # 2 active members
184 assert_equal 2, @ecookbook.members.size
184 assert_equal 2, @ecookbook.members.size
185 # and 1 is locked
185 # and 1 is locked
186 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
186 assert_equal 3, Member.where('project_id = ?', @ecookbook.id).all.size
187 # some boards
187 # some boards
188 assert @ecookbook.boards.any?
188 assert @ecookbook.boards.any?
189
189
190 @ecookbook.destroy
190 @ecookbook.destroy
191 # make sure that the project non longer exists
191 # make sure that the project non longer exists
192 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
192 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
193 # make sure related data was removed
193 # make sure related data was removed
194 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
194 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
195 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
195 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
196 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
196 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
197 end
197 end
198
198
199 def test_destroy_should_destroy_subtasks
199 def test_destroy_should_destroy_subtasks
200 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
200 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
201 issues[0].update_attribute :parent_issue_id, issues[1].id
201 issues[0].update_attribute :parent_issue_id, issues[1].id
202 issues[2].update_attribute :parent_issue_id, issues[1].id
202 issues[2].update_attribute :parent_issue_id, issues[1].id
203 assert_equal 2, issues[1].children.count
203 assert_equal 2, issues[1].children.count
204
204
205 assert_nothing_raised do
205 assert_nothing_raised do
206 Project.find(1).destroy
206 Project.find(1).destroy
207 end
207 end
208 assert Issue.find_all_by_id(issues.map(&:id)).empty?
208 assert Issue.find_all_by_id(issues.map(&:id)).empty?
209 end
209 end
210
210
211 def test_destroying_root_projects_should_clear_data
211 def test_destroying_root_projects_should_clear_data
212 Project.roots.each do |root|
212 Project.roots.each do |root|
213 root.destroy
213 root.destroy
214 end
214 end
215
215
216 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
216 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
217 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
217 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
218 assert_equal 0, MemberRole.count
218 assert_equal 0, MemberRole.count
219 assert_equal 0, Issue.count
219 assert_equal 0, Issue.count
220 assert_equal 0, Journal.count
220 assert_equal 0, Journal.count
221 assert_equal 0, JournalDetail.count
221 assert_equal 0, JournalDetail.count
222 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
222 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
223 assert_equal 0, EnabledModule.count
223 assert_equal 0, EnabledModule.count
224 assert_equal 0, IssueCategory.count
224 assert_equal 0, IssueCategory.count
225 assert_equal 0, IssueRelation.count
225 assert_equal 0, IssueRelation.count
226 assert_equal 0, Board.count
226 assert_equal 0, Board.count
227 assert_equal 0, Message.count
227 assert_equal 0, Message.count
228 assert_equal 0, News.count
228 assert_equal 0, News.count
229 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
229 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
230 assert_equal 0, Repository.count
230 assert_equal 0, Repository.count
231 assert_equal 0, Changeset.count
231 assert_equal 0, Changeset.count
232 assert_equal 0, Change.count
232 assert_equal 0, Change.count
233 assert_equal 0, Comment.count
233 assert_equal 0, Comment.count
234 assert_equal 0, TimeEntry.count
234 assert_equal 0, TimeEntry.count
235 assert_equal 0, Version.count
235 assert_equal 0, Version.count
236 assert_equal 0, Watcher.count
236 assert_equal 0, Watcher.count
237 assert_equal 0, Wiki.count
237 assert_equal 0, Wiki.count
238 assert_equal 0, WikiPage.count
238 assert_equal 0, WikiPage.count
239 assert_equal 0, WikiContent.count
239 assert_equal 0, WikiContent.count
240 assert_equal 0, WikiContent::Version.count
240 assert_equal 0, WikiContent::Version.count
241 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
241 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
242 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
242 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
243 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
243 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
244 end
244 end
245
245
246 def test_move_an_orphan_project_to_a_root_project
246 def test_move_an_orphan_project_to_a_root_project
247 sub = Project.find(2)
247 sub = Project.find(2)
248 sub.set_parent! @ecookbook
248 sub.set_parent! @ecookbook
249 assert_equal @ecookbook.id, sub.parent.id
249 assert_equal @ecookbook.id, sub.parent.id
250 @ecookbook.reload
250 @ecookbook.reload
251 assert_equal 4, @ecookbook.children.size
251 assert_equal 4, @ecookbook.children.size
252 end
252 end
253
253
254 def test_move_an_orphan_project_to_a_subproject
254 def test_move_an_orphan_project_to_a_subproject
255 sub = Project.find(2)
255 sub = Project.find(2)
256 assert sub.set_parent!(@ecookbook_sub1)
256 assert sub.set_parent!(@ecookbook_sub1)
257 end
257 end
258
258
259 def test_move_a_root_project_to_a_project
259 def test_move_a_root_project_to_a_project
260 sub = @ecookbook
260 sub = @ecookbook
261 assert sub.set_parent!(Project.find(2))
261 assert sub.set_parent!(Project.find(2))
262 end
262 end
263
263
264 def test_should_not_move_a_project_to_its_children
264 def test_should_not_move_a_project_to_its_children
265 sub = @ecookbook
265 sub = @ecookbook
266 assert !(sub.set_parent!(Project.find(3)))
266 assert !(sub.set_parent!(Project.find(3)))
267 end
267 end
268
268
269 def test_set_parent_should_add_roots_in_alphabetical_order
269 def test_set_parent_should_add_roots_in_alphabetical_order
270 ProjectCustomField.delete_all
270 ProjectCustomField.delete_all
271 Project.delete_all
271 Project.delete_all
272 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
272 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
273 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
273 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
274 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
274 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
275 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
275 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
276
276
277 assert_equal 4, Project.count
277 assert_equal 4, Project.count
278 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
278 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
279 end
279 end
280
280
281 def test_set_parent_should_add_children_in_alphabetical_order
281 def test_set_parent_should_add_children_in_alphabetical_order
282 ProjectCustomField.delete_all
282 ProjectCustomField.delete_all
283 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
283 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
284 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
284 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
285 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
285 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
286 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
286 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
287 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
287 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
288
288
289 parent.reload
289 parent.reload
290 assert_equal 4, parent.children.size
290 assert_equal 4, parent.children.size
291 assert_equal parent.children.all.sort_by(&:name), parent.children.all
291 assert_equal parent.children.all.sort_by(&:name), parent.children.all
292 end
292 end
293
293
294 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
294 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
295 # Parent issue with a hierarchy project's fixed version
295 # Parent issue with a hierarchy project's fixed version
296 parent_issue = Issue.find(1)
296 parent_issue = Issue.find(1)
297 parent_issue.update_attribute(:fixed_version_id, 4)
297 parent_issue.update_attribute(:fixed_version_id, 4)
298 parent_issue.reload
298 parent_issue.reload
299 assert_equal 4, parent_issue.fixed_version_id
299 assert_equal 4, parent_issue.fixed_version_id
300
300
301 # Should keep fixed versions for the issues
301 # Should keep fixed versions for the issues
302 issue_with_local_fixed_version = Issue.find(5)
302 issue_with_local_fixed_version = Issue.find(5)
303 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
303 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
304 issue_with_local_fixed_version.reload
304 issue_with_local_fixed_version.reload
305 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
305 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
306
306
307 # Local issue with hierarchy fixed_version
307 # Local issue with hierarchy fixed_version
308 issue_with_hierarchy_fixed_version = Issue.find(13)
308 issue_with_hierarchy_fixed_version = Issue.find(13)
309 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
309 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
310 issue_with_hierarchy_fixed_version.reload
310 issue_with_hierarchy_fixed_version.reload
311 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
311 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
312
312
313 # Move project out of the issue's hierarchy
313 # Move project out of the issue's hierarchy
314 moved_project = Project.find(3)
314 moved_project = Project.find(3)
315 moved_project.set_parent!(Project.find(2))
315 moved_project.set_parent!(Project.find(2))
316 parent_issue.reload
316 parent_issue.reload
317 issue_with_local_fixed_version.reload
317 issue_with_local_fixed_version.reload
318 issue_with_hierarchy_fixed_version.reload
318 issue_with_hierarchy_fixed_version.reload
319
319
320 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
320 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
321 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
321 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
322 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
322 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
323 end
323 end
324
324
325 def test_parent
325 def test_parent
326 p = Project.find(6).parent
326 p = Project.find(6).parent
327 assert p.is_a?(Project)
327 assert p.is_a?(Project)
328 assert_equal 5, p.id
328 assert_equal 5, p.id
329 end
329 end
330
330
331 def test_ancestors
331 def test_ancestors
332 a = Project.find(6).ancestors
332 a = Project.find(6).ancestors
333 assert a.first.is_a?(Project)
333 assert a.first.is_a?(Project)
334 assert_equal [1, 5], a.collect(&:id)
334 assert_equal [1, 5], a.collect(&:id)
335 end
335 end
336
336
337 def test_root
337 def test_root
338 r = Project.find(6).root
338 r = Project.find(6).root
339 assert r.is_a?(Project)
339 assert r.is_a?(Project)
340 assert_equal 1, r.id
340 assert_equal 1, r.id
341 end
341 end
342
342
343 def test_children
343 def test_children
344 c = Project.find(1).children
344 c = Project.find(1).children
345 assert c.first.is_a?(Project)
345 assert c.first.is_a?(Project)
346 assert_equal [5, 3, 4], c.collect(&:id)
346 assert_equal [5, 3, 4], c.collect(&:id)
347 end
347 end
348
348
349 def test_descendants
349 def test_descendants
350 d = Project.find(1).descendants
350 d = Project.find(1).descendants
351 assert d.first.is_a?(Project)
351 assert d.first.is_a?(Project)
352 assert_equal [5, 6, 3, 4], d.collect(&:id)
352 assert_equal [5, 6, 3, 4], d.collect(&:id)
353 end
353 end
354
354
355 def test_allowed_parents_should_be_empty_for_non_member_user
355 def test_allowed_parents_should_be_empty_for_non_member_user
356 Role.non_member.add_permission!(:add_project)
356 Role.non_member.add_permission!(:add_project)
357 user = User.find(9)
357 user = User.find(9)
358 assert user.memberships.empty?
358 assert user.memberships.empty?
359 User.current = user
359 User.current = user
360 assert Project.new.allowed_parents.compact.empty?
360 assert Project.new.allowed_parents.compact.empty?
361 end
361 end
362
362
363 def test_allowed_parents_with_add_subprojects_permission
363 def test_allowed_parents_with_add_subprojects_permission
364 Role.find(1).remove_permission!(:add_project)
364 Role.find(1).remove_permission!(:add_project)
365 Role.find(1).add_permission!(:add_subprojects)
365 Role.find(1).add_permission!(:add_subprojects)
366 User.current = User.find(2)
366 User.current = User.find(2)
367 # new project
367 # new project
368 assert !Project.new.allowed_parents.include?(nil)
368 assert !Project.new.allowed_parents.include?(nil)
369 assert Project.new.allowed_parents.include?(Project.find(1))
369 assert Project.new.allowed_parents.include?(Project.find(1))
370 # existing root project
370 # existing root project
371 assert Project.find(1).allowed_parents.include?(nil)
371 assert Project.find(1).allowed_parents.include?(nil)
372 # existing child
372 # existing child
373 assert Project.find(3).allowed_parents.include?(Project.find(1))
373 assert Project.find(3).allowed_parents.include?(Project.find(1))
374 assert !Project.find(3).allowed_parents.include?(nil)
374 assert !Project.find(3).allowed_parents.include?(nil)
375 end
375 end
376
376
377 def test_allowed_parents_with_add_project_permission
377 def test_allowed_parents_with_add_project_permission
378 Role.find(1).add_permission!(:add_project)
378 Role.find(1).add_permission!(:add_project)
379 Role.find(1).remove_permission!(:add_subprojects)
379 Role.find(1).remove_permission!(:add_subprojects)
380 User.current = User.find(2)
380 User.current = User.find(2)
381 # new project
381 # new project
382 assert Project.new.allowed_parents.include?(nil)
382 assert Project.new.allowed_parents.include?(nil)
383 assert !Project.new.allowed_parents.include?(Project.find(1))
383 assert !Project.new.allowed_parents.include?(Project.find(1))
384 # existing root project
384 # existing root project
385 assert Project.find(1).allowed_parents.include?(nil)
385 assert Project.find(1).allowed_parents.include?(nil)
386 # existing child
386 # existing child
387 assert Project.find(3).allowed_parents.include?(Project.find(1))
387 assert Project.find(3).allowed_parents.include?(Project.find(1))
388 assert Project.find(3).allowed_parents.include?(nil)
388 assert Project.find(3).allowed_parents.include?(nil)
389 end
389 end
390
390
391 def test_allowed_parents_with_add_project_and_subprojects_permission
391 def test_allowed_parents_with_add_project_and_subprojects_permission
392 Role.find(1).add_permission!(:add_project)
392 Role.find(1).add_permission!(:add_project)
393 Role.find(1).add_permission!(:add_subprojects)
393 Role.find(1).add_permission!(:add_subprojects)
394 User.current = User.find(2)
394 User.current = User.find(2)
395 # new project
395 # new project
396 assert Project.new.allowed_parents.include?(nil)
396 assert Project.new.allowed_parents.include?(nil)
397 assert Project.new.allowed_parents.include?(Project.find(1))
397 assert Project.new.allowed_parents.include?(Project.find(1))
398 # existing root project
398 # existing root project
399 assert Project.find(1).allowed_parents.include?(nil)
399 assert Project.find(1).allowed_parents.include?(nil)
400 # existing child
400 # existing child
401 assert Project.find(3).allowed_parents.include?(Project.find(1))
401 assert Project.find(3).allowed_parents.include?(Project.find(1))
402 assert Project.find(3).allowed_parents.include?(nil)
402 assert Project.find(3).allowed_parents.include?(nil)
403 end
403 end
404
404
405 def test_users_by_role
405 def test_users_by_role
406 users_by_role = Project.find(1).users_by_role
406 users_by_role = Project.find(1).users_by_role
407 assert_kind_of Hash, users_by_role
407 assert_kind_of Hash, users_by_role
408 role = Role.find(1)
408 role = Role.find(1)
409 assert_kind_of Array, users_by_role[role]
409 assert_kind_of Array, users_by_role[role]
410 assert users_by_role[role].include?(User.find(2))
410 assert users_by_role[role].include?(User.find(2))
411 end
411 end
412
412
413 def test_rolled_up_trackers
413 def test_rolled_up_trackers
414 parent = Project.find(1)
414 parent = Project.find(1)
415 parent.trackers = Tracker.find([1,2])
415 parent.trackers = Tracker.find([1,2])
416 child = parent.children.find(3)
416 child = parent.children.find(3)
417
417
418 assert_equal [1, 2], parent.tracker_ids
418 assert_equal [1, 2], parent.tracker_ids
419 assert_equal [2, 3], child.trackers.collect(&:id)
419 assert_equal [2, 3], child.trackers.collect(&:id)
420
420
421 assert_kind_of Tracker, parent.rolled_up_trackers.first
421 assert_kind_of Tracker, parent.rolled_up_trackers.first
422 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
422 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
423
423
424 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
424 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
425 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
425 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
426 end
426 end
427
427
428 def test_rolled_up_trackers_should_ignore_archived_subprojects
428 def test_rolled_up_trackers_should_ignore_archived_subprojects
429 parent = Project.find(1)
429 parent = Project.find(1)
430 parent.trackers = Tracker.find([1,2])
430 parent.trackers = Tracker.find([1,2])
431 child = parent.children.find(3)
431 child = parent.children.find(3)
432 child.trackers = Tracker.find([1,3])
432 child.trackers = Tracker.find([1,3])
433 parent.children.each(&:archive)
433 parent.children.each(&:archive)
434
434
435 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
435 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
436 end
436 end
437
437
438 context "#rolled_up_versions" do
438 context "#rolled_up_versions" do
439 setup do
439 setup do
440 @project = Project.generate!
440 @project = Project.generate!
441 @parent_version_1 = Version.generate!(:project => @project)
441 @parent_version_1 = Version.generate!(:project => @project)
442 @parent_version_2 = Version.generate!(:project => @project)
442 @parent_version_2 = Version.generate!(:project => @project)
443 end
443 end
444
444
445 should "include the versions for the current project" do
445 should "include the versions for the current project" do
446 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
446 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
447 end
447 end
448
448
449 should "include versions for a subproject" do
449 should "include versions for a subproject" do
450 @subproject = Project.generate!
450 @subproject = Project.generate!
451 @subproject.set_parent!(@project)
451 @subproject.set_parent!(@project)
452 @subproject_version = Version.generate!(:project => @subproject)
452 @subproject_version = Version.generate!(:project => @subproject)
453
453
454 assert_same_elements [
454 assert_same_elements [
455 @parent_version_1,
455 @parent_version_1,
456 @parent_version_2,
456 @parent_version_2,
457 @subproject_version
457 @subproject_version
458 ], @project.rolled_up_versions
458 ], @project.rolled_up_versions
459 end
459 end
460
460
461 should "include versions for a sub-subproject" do
461 should "include versions for a sub-subproject" do
462 @subproject = Project.generate!
462 @subproject = Project.generate!
463 @subproject.set_parent!(@project)
463 @subproject.set_parent!(@project)
464 @sub_subproject = Project.generate!
464 @sub_subproject = Project.generate!
465 @sub_subproject.set_parent!(@subproject)
465 @sub_subproject.set_parent!(@subproject)
466 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
466 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
467
467
468 @project.reload
468 @project.reload
469
469
470 assert_same_elements [
470 assert_same_elements [
471 @parent_version_1,
471 @parent_version_1,
472 @parent_version_2,
472 @parent_version_2,
473 @sub_subproject_version
473 @sub_subproject_version
474 ], @project.rolled_up_versions
474 ], @project.rolled_up_versions
475 end
475 end
476
476
477 should "only check active projects" do
477 should "only check active projects" do
478 @subproject = Project.generate!
478 @subproject = Project.generate!
479 @subproject.set_parent!(@project)
479 @subproject.set_parent!(@project)
480 @subproject_version = Version.generate!(:project => @subproject)
480 @subproject_version = Version.generate!(:project => @subproject)
481 assert @subproject.archive
481 assert @subproject.archive
482
482
483 @project.reload
483 @project.reload
484
484
485 assert !@subproject.active?
485 assert !@subproject.active?
486 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
486 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
487 end
487 end
488 end
488 end
489
489
490 def test_shared_versions_none_sharing
490 def test_shared_versions_none_sharing
491 p = Project.find(5)
491 p = Project.find(5)
492 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
492 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
493 assert p.shared_versions.include?(v)
493 assert p.shared_versions.include?(v)
494 assert !p.children.first.shared_versions.include?(v)
494 assert !p.children.first.shared_versions.include?(v)
495 assert !p.root.shared_versions.include?(v)
495 assert !p.root.shared_versions.include?(v)
496 assert !p.siblings.first.shared_versions.include?(v)
496 assert !p.siblings.first.shared_versions.include?(v)
497 assert !p.root.siblings.first.shared_versions.include?(v)
497 assert !p.root.siblings.first.shared_versions.include?(v)
498 end
498 end
499
499
500 def test_shared_versions_descendants_sharing
500 def test_shared_versions_descendants_sharing
501 p = Project.find(5)
501 p = Project.find(5)
502 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
502 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
503 assert p.shared_versions.include?(v)
503 assert p.shared_versions.include?(v)
504 assert p.children.first.shared_versions.include?(v)
504 assert p.children.first.shared_versions.include?(v)
505 assert !p.root.shared_versions.include?(v)
505 assert !p.root.shared_versions.include?(v)
506 assert !p.siblings.first.shared_versions.include?(v)
506 assert !p.siblings.first.shared_versions.include?(v)
507 assert !p.root.siblings.first.shared_versions.include?(v)
507 assert !p.root.siblings.first.shared_versions.include?(v)
508 end
508 end
509
509
510 def test_shared_versions_hierarchy_sharing
510 def test_shared_versions_hierarchy_sharing
511 p = Project.find(5)
511 p = Project.find(5)
512 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
512 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
513 assert p.shared_versions.include?(v)
513 assert p.shared_versions.include?(v)
514 assert p.children.first.shared_versions.include?(v)
514 assert p.children.first.shared_versions.include?(v)
515 assert p.root.shared_versions.include?(v)
515 assert p.root.shared_versions.include?(v)
516 assert !p.siblings.first.shared_versions.include?(v)
516 assert !p.siblings.first.shared_versions.include?(v)
517 assert !p.root.siblings.first.shared_versions.include?(v)
517 assert !p.root.siblings.first.shared_versions.include?(v)
518 end
518 end
519
519
520 def test_shared_versions_tree_sharing
520 def test_shared_versions_tree_sharing
521 p = Project.find(5)
521 p = Project.find(5)
522 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
522 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
523 assert p.shared_versions.include?(v)
523 assert p.shared_versions.include?(v)
524 assert p.children.first.shared_versions.include?(v)
524 assert p.children.first.shared_versions.include?(v)
525 assert p.root.shared_versions.include?(v)
525 assert p.root.shared_versions.include?(v)
526 assert p.siblings.first.shared_versions.include?(v)
526 assert p.siblings.first.shared_versions.include?(v)
527 assert !p.root.siblings.first.shared_versions.include?(v)
527 assert !p.root.siblings.first.shared_versions.include?(v)
528 end
528 end
529
529
530 def test_shared_versions_system_sharing
530 def test_shared_versions_system_sharing
531 p = Project.find(5)
531 p = Project.find(5)
532 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
532 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
533 assert p.shared_versions.include?(v)
533 assert p.shared_versions.include?(v)
534 assert p.children.first.shared_versions.include?(v)
534 assert p.children.first.shared_versions.include?(v)
535 assert p.root.shared_versions.include?(v)
535 assert p.root.shared_versions.include?(v)
536 assert p.siblings.first.shared_versions.include?(v)
536 assert p.siblings.first.shared_versions.include?(v)
537 assert p.root.siblings.first.shared_versions.include?(v)
537 assert p.root.siblings.first.shared_versions.include?(v)
538 end
538 end
539
539
540 def test_shared_versions
540 def test_shared_versions
541 parent = Project.find(1)
541 parent = Project.find(1)
542 child = parent.children.find(3)
542 child = parent.children.find(3)
543 private_child = parent.children.find(5)
543 private_child = parent.children.find(5)
544
544
545 assert_equal [1,2,3], parent.version_ids.sort
545 assert_equal [1,2,3], parent.version_ids.sort
546 assert_equal [4], child.version_ids
546 assert_equal [4], child.version_ids
547 assert_equal [6], private_child.version_ids
547 assert_equal [6], private_child.version_ids
548 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
548 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
549
549
550 assert_equal 6, parent.shared_versions.size
550 assert_equal 6, parent.shared_versions.size
551 parent.shared_versions.each do |version|
551 parent.shared_versions.each do |version|
552 assert_kind_of Version, version
552 assert_kind_of Version, version
553 end
553 end
554
554
555 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
555 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
556 end
556 end
557
557
558 def test_shared_versions_should_ignore_archived_subprojects
558 def test_shared_versions_should_ignore_archived_subprojects
559 parent = Project.find(1)
559 parent = Project.find(1)
560 child = parent.children.find(3)
560 child = parent.children.find(3)
561 child.archive
561 child.archive
562 parent.reload
562 parent.reload
563
563
564 assert_equal [1,2,3], parent.version_ids.sort
564 assert_equal [1,2,3], parent.version_ids.sort
565 assert_equal [4], child.version_ids
565 assert_equal [4], child.version_ids
566 assert !parent.shared_versions.collect(&:id).include?(4)
566 assert !parent.shared_versions.collect(&:id).include?(4)
567 end
567 end
568
568
569 def test_shared_versions_visible_to_user
569 def test_shared_versions_visible_to_user
570 user = User.find(3)
570 user = User.find(3)
571 parent = Project.find(1)
571 parent = Project.find(1)
572 child = parent.children.find(5)
572 child = parent.children.find(5)
573
573
574 assert_equal [1,2,3], parent.version_ids.sort
574 assert_equal [1,2,3], parent.version_ids.sort
575 assert_equal [6], child.version_ids
575 assert_equal [6], child.version_ids
576
576
577 versions = parent.shared_versions.visible(user)
577 versions = parent.shared_versions.visible(user)
578
578
579 assert_equal 4, versions.size
579 assert_equal 4, versions.size
580 versions.each do |version|
580 versions.each do |version|
581 assert_kind_of Version, version
581 assert_kind_of Version, version
582 end
582 end
583
583
584 assert !versions.collect(&:id).include?(6)
584 assert !versions.collect(&:id).include?(6)
585 end
585 end
586
586
587 def test_shared_versions_for_new_project_should_include_system_shared_versions
587 def test_shared_versions_for_new_project_should_include_system_shared_versions
588 p = Project.find(5)
588 p = Project.find(5)
589 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
589 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
590
590
591 assert_include v, Project.new.shared_versions
591 assert_include v, Project.new.shared_versions
592 end
592 end
593
593
594 def test_next_identifier
594 def test_next_identifier
595 ProjectCustomField.delete_all
595 ProjectCustomField.delete_all
596 Project.create!(:name => 'last', :identifier => 'p2008040')
596 Project.create!(:name => 'last', :identifier => 'p2008040')
597 assert_equal 'p2008041', Project.next_identifier
597 assert_equal 'p2008041', Project.next_identifier
598 end
598 end
599
599
600 def test_next_identifier_first_project
600 def test_next_identifier_first_project
601 Project.delete_all
601 Project.delete_all
602 assert_nil Project.next_identifier
602 assert_nil Project.next_identifier
603 end
603 end
604
604
605 def test_enabled_module_names
605 def test_enabled_module_names
606 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
606 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
607 project = Project.new
607 project = Project.new
608
608
609 project.enabled_module_names = %w(issue_tracking news)
609 project.enabled_module_names = %w(issue_tracking news)
610 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
610 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
611 end
611 end
612 end
612 end
613
613
614 context "enabled_modules" do
614 context "enabled_modules" do
615 setup do
615 setup do
616 @project = Project.find(1)
616 @project = Project.find(1)
617 end
617 end
618
618
619 should "define module by names and preserve ids" do
619 should "define module by names and preserve ids" do
620 # Remove one module
620 # Remove one module
621 modules = @project.enabled_modules.slice(0..-2)
621 modules = @project.enabled_modules.slice(0..-2)
622 assert modules.any?
622 assert modules.any?
623 assert_difference 'EnabledModule.count', -1 do
623 assert_difference 'EnabledModule.count', -1 do
624 @project.enabled_module_names = modules.collect(&:name)
624 @project.enabled_module_names = modules.collect(&:name)
625 end
625 end
626 @project.reload
626 @project.reload
627 # Ids should be preserved
627 # Ids should be preserved
628 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
628 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
629 end
629 end
630
630
631 should "enable a module" do
631 should "enable a module" do
632 @project.enabled_module_names = []
632 @project.enabled_module_names = []
633 @project.reload
633 @project.reload
634 assert_equal [], @project.enabled_module_names
634 assert_equal [], @project.enabled_module_names
635 #with string
635 #with string
636 @project.enable_module!("issue_tracking")
636 @project.enable_module!("issue_tracking")
637 assert_equal ["issue_tracking"], @project.enabled_module_names
637 assert_equal ["issue_tracking"], @project.enabled_module_names
638 #with symbol
638 #with symbol
639 @project.enable_module!(:gantt)
639 @project.enable_module!(:gantt)
640 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
640 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
641 #don't add a module twice
641 #don't add a module twice
642 @project.enable_module!("issue_tracking")
642 @project.enable_module!("issue_tracking")
643 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
643 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
644 end
644 end
645
645
646 should "disable a module" do
646 should "disable a module" do
647 #with string
647 #with string
648 assert @project.enabled_module_names.include?("issue_tracking")
648 assert @project.enabled_module_names.include?("issue_tracking")
649 @project.disable_module!("issue_tracking")
649 @project.disable_module!("issue_tracking")
650 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
650 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
651 #with symbol
651 #with symbol
652 assert @project.enabled_module_names.include?("gantt")
652 assert @project.enabled_module_names.include?("gantt")
653 @project.disable_module!(:gantt)
653 @project.disable_module!(:gantt)
654 assert ! @project.reload.enabled_module_names.include?("gantt")
654 assert ! @project.reload.enabled_module_names.include?("gantt")
655 #with EnabledModule object
655 #with EnabledModule object
656 first_module = @project.enabled_modules.first
656 first_module = @project.enabled_modules.first
657 @project.disable_module!(first_module)
657 @project.disable_module!(first_module)
658 assert ! @project.reload.enabled_module_names.include?(first_module.name)
658 assert ! @project.reload.enabled_module_names.include?(first_module.name)
659 end
659 end
660 end
660 end
661
661
662 def test_enabled_module_names_should_not_recreate_enabled_modules
662 def test_enabled_module_names_should_not_recreate_enabled_modules
663 project = Project.find(1)
663 project = Project.find(1)
664 # Remove one module
664 # Remove one module
665 modules = project.enabled_modules.slice(0..-2)
665 modules = project.enabled_modules.slice(0..-2)
666 assert modules.any?
666 assert modules.any?
667 assert_difference 'EnabledModule.count', -1 do
667 assert_difference 'EnabledModule.count', -1 do
668 project.enabled_module_names = modules.collect(&:name)
668 project.enabled_module_names = modules.collect(&:name)
669 end
669 end
670 project.reload
670 project.reload
671 # Ids should be preserved
671 # Ids should be preserved
672 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
672 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
673 end
673 end
674
674
675 def test_copy_from_existing_project
675 def test_copy_from_existing_project
676 source_project = Project.find(1)
676 source_project = Project.find(1)
677 copied_project = Project.copy_from(1)
677 copied_project = Project.copy_from(1)
678
678
679 assert copied_project
679 assert copied_project
680 # Cleared attributes
680 # Cleared attributes
681 assert copied_project.id.blank?
681 assert copied_project.id.blank?
682 assert copied_project.name.blank?
682 assert copied_project.name.blank?
683 assert copied_project.identifier.blank?
683 assert copied_project.identifier.blank?
684
684
685 # Duplicated attributes
685 # Duplicated attributes
686 assert_equal source_project.description, copied_project.description
686 assert_equal source_project.description, copied_project.description
687 assert_equal source_project.enabled_modules, copied_project.enabled_modules
687 assert_equal source_project.enabled_modules, copied_project.enabled_modules
688 assert_equal source_project.trackers, copied_project.trackers
688 assert_equal source_project.trackers, copied_project.trackers
689
689
690 # Default attributes
690 # Default attributes
691 assert_equal 1, copied_project.status
691 assert_equal 1, copied_project.status
692 end
692 end
693
693
694 def test_activities_should_use_the_system_activities
694 def test_activities_should_use_the_system_activities
695 project = Project.find(1)
695 project = Project.find(1)
696 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
696 assert_equal project.activities, TimeEntryActivity.where(:active => true).all
697 end
697 end
698
698
699
699
700 def test_activities_should_use_the_project_specific_activities
700 def test_activities_should_use_the_project_specific_activities
701 project = Project.find(1)
701 project = Project.find(1)
702 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
702 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
703 assert overridden_activity.save!
703 assert overridden_activity.save!
704
704
705 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
705 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
706 end
706 end
707
707
708 def test_activities_should_not_include_the_inactive_project_specific_activities
708 def test_activities_should_not_include_the_inactive_project_specific_activities
709 project = Project.find(1)
709 project = Project.find(1)
710 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
710 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
711 assert overridden_activity.save!
711 assert overridden_activity.save!
712
712
713 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
713 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
714 end
714 end
715
715
716 def test_activities_should_not_include_project_specific_activities_from_other_projects
716 def test_activities_should_not_include_project_specific_activities_from_other_projects
717 project = Project.find(1)
717 project = Project.find(1)
718 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
718 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
719 assert overridden_activity.save!
719 assert overridden_activity.save!
720
720
721 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
721 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
722 end
722 end
723
723
724 def test_activities_should_handle_nils
724 def test_activities_should_handle_nils
725 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
725 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
726 TimeEntryActivity.delete_all
726 TimeEntryActivity.delete_all
727
727
728 # No activities
728 # No activities
729 project = Project.find(1)
729 project = Project.find(1)
730 assert project.activities.empty?
730 assert project.activities.empty?
731
731
732 # No system, one overridden
732 # No system, one overridden
733 assert overridden_activity.save!
733 assert overridden_activity.save!
734 project.reload
734 project.reload
735 assert_equal [overridden_activity], project.activities
735 assert_equal [overridden_activity], project.activities
736 end
736 end
737
737
738 def test_activities_should_override_system_activities_with_project_activities
738 def test_activities_should_override_system_activities_with_project_activities
739 project = Project.find(1)
739 project = Project.find(1)
740 parent_activity = TimeEntryActivity.find(:first)
740 parent_activity = TimeEntryActivity.find(:first)
741 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
741 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
742 assert overridden_activity.save!
742 assert overridden_activity.save!
743
743
744 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
744 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
745 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
745 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
746 end
746 end
747
747
748 def test_activities_should_include_inactive_activities_if_specified
748 def test_activities_should_include_inactive_activities_if_specified
749 project = Project.find(1)
749 project = Project.find(1)
750 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
750 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
751 assert overridden_activity.save!
751 assert overridden_activity.save!
752
752
753 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
753 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
754 end
754 end
755
755
756 test 'activities should not include active System activities if the project has an override that is inactive' do
756 test 'activities should not include active System activities if the project has an override that is inactive' do
757 project = Project.find(1)
757 project = Project.find(1)
758 system_activity = TimeEntryActivity.find_by_name('Design')
758 system_activity = TimeEntryActivity.find_by_name('Design')
759 assert system_activity.active?
759 assert system_activity.active?
760 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
760 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
761 assert overridden_activity.save!
761 assert overridden_activity.save!
762
762
763 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
763 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
764 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
764 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
765 end
765 end
766
766
767 def test_close_completed_versions
767 def test_close_completed_versions
768 Version.update_all("status = 'open'")
768 Version.update_all("status = 'open'")
769 project = Project.find(1)
769 project = Project.find(1)
770 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
770 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
771 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
771 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
772 project.close_completed_versions
772 project.close_completed_versions
773 project.reload
773 project.reload
774 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
774 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
775 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
775 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
776 end
776 end
777
777
778 context "Project#copy" do
778 context "Project#copy" do
779 setup do
779 setup do
780 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
780 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
781 Project.destroy_all :identifier => "copy-test"
781 Project.destroy_all :identifier => "copy-test"
782 @source_project = Project.find(2)
782 @source_project = Project.find(2)
783 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
783 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
784 @project.trackers = @source_project.trackers
784 @project.trackers = @source_project.trackers
785 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
785 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
786 end
786 end
787
787
788 should "copy issues" do
788 should "copy issues" do
789 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
789 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
790 :subject => "copy issue status",
790 :subject => "copy issue status",
791 :tracker_id => 1,
791 :tracker_id => 1,
792 :assigned_to_id => 2,
792 :assigned_to_id => 2,
793 :project_id => @source_project.id)
793 :project_id => @source_project.id)
794 assert @project.valid?
794 assert @project.valid?
795 assert @project.issues.empty?
795 assert @project.issues.empty?
796 assert @project.copy(@source_project)
796 assert @project.copy(@source_project)
797
797
798 assert_equal @source_project.issues.size, @project.issues.size
798 assert_equal @source_project.issues.size, @project.issues.size
799 @project.issues.each do |issue|
799 @project.issues.each do |issue|
800 assert issue.valid?
800 assert issue.valid?
801 assert ! issue.assigned_to.blank?
801 assert ! issue.assigned_to.blank?
802 assert_equal @project, issue.project
802 assert_equal @project, issue.project
803 end
803 end
804
804
805 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
805 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
806 assert copied_issue
806 assert copied_issue
807 assert copied_issue.status
807 assert copied_issue.status
808 assert_equal "Closed", copied_issue.status.name
808 assert_equal "Closed", copied_issue.status.name
809 end
809 end
810
810
811 should "copy issues assigned to a locked version" do
811 should "copy issues assigned to a locked version" do
812 User.current = User.find(1)
812 User.current = User.find(1)
813 assigned_version = Version.generate!(:name => "Assigned Issues")
813 assigned_version = Version.generate!(:name => "Assigned Issues")
814 @source_project.versions << assigned_version
814 @source_project.versions << assigned_version
815 Issue.generate!(:project => @source_project,
815 Issue.generate!(:project => @source_project,
816 :fixed_version_id => assigned_version.id,
816 :fixed_version_id => assigned_version.id,
817 :subject => "copy issues assigned to a locked version")
817 :subject => "copy issues assigned to a locked version")
818 assigned_version.update_attribute :status, 'locked'
818 assigned_version.update_attribute :status, 'locked'
819
819
820 assert @project.copy(@source_project)
820 assert @project.copy(@source_project)
821 @project.reload
821 @project.reload
822 copied_issue = @project.issues.first(:conditions => {:subject => "copy issues assigned to a locked version"})
822 copied_issue = @project.issues.first(:conditions => {:subject => "copy issues assigned to a locked version"})
823
823
824 assert copied_issue
824 assert copied_issue
825 assert copied_issue.fixed_version
825 assert copied_issue.fixed_version
826 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
826 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
827 assert_equal 'locked', copied_issue.fixed_version.status
827 assert_equal 'locked', copied_issue.fixed_version.status
828 end
828 end
829
829
830 should "change the new issues to use the copied version" do
830 should "change the new issues to use the copied version" do
831 User.current = User.find(1)
831 User.current = User.find(1)
832 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
832 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
833 @source_project.versions << assigned_version
833 @source_project.versions << assigned_version
834 assert_equal 3, @source_project.versions.size
834 assert_equal 3, @source_project.versions.size
835 Issue.generate!(:project => @source_project,
835 Issue.generate!(:project => @source_project,
836 :fixed_version_id => assigned_version.id,
836 :fixed_version_id => assigned_version.id,
837 :subject => "change the new issues to use the copied version")
837 :subject => "change the new issues to use the copied version")
838
838
839 assert @project.copy(@source_project)
839 assert @project.copy(@source_project)
840 @project.reload
840 @project.reload
841 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
841 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
842
842
843 assert copied_issue
843 assert copied_issue
844 assert copied_issue.fixed_version
844 assert copied_issue.fixed_version
845 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
845 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
846 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
846 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
847 end
847 end
848
848
849 should "keep target shared versions from other project" do
849 should "keep target shared versions from other project" do
850 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system')
850 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open', :project_id => 1, :sharing => 'system')
851 issue = Issue.generate!(:project => @source_project,
851 issue = Issue.generate!(:project => @source_project,
852 :fixed_version => assigned_version,
852 :fixed_version => assigned_version,
853 :subject => "keep target shared versions")
853 :subject => "keep target shared versions")
854
854
855 assert @project.copy(@source_project)
855 assert @project.copy(@source_project)
856 @project.reload
856 @project.reload
857 copied_issue = @project.issues.first(:conditions => {:subject => "keep target shared versions"})
857 copied_issue = @project.issues.first(:conditions => {:subject => "keep target shared versions"})
858
858
859 assert copied_issue
859 assert copied_issue
860 assert_equal assigned_version, copied_issue.fixed_version
860 assert_equal assigned_version, copied_issue.fixed_version
861 end
861 end
862
862
863 should "copy issue relations" do
863 should "copy issue relations" do
864 Setting.cross_project_issue_relations = '1'
864 Setting.cross_project_issue_relations = '1'
865
865
866 second_issue = Issue.generate!(:status_id => 5,
866 second_issue = Issue.generate!(:status_id => 5,
867 :subject => "copy issue relation",
867 :subject => "copy issue relation",
868 :tracker_id => 1,
868 :tracker_id => 1,
869 :assigned_to_id => 2,
869 :assigned_to_id => 2,
870 :project_id => @source_project.id)
870 :project_id => @source_project.id)
871 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
871 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
872 :issue_to => second_issue,
872 :issue_to => second_issue,
873 :relation_type => "relates")
873 :relation_type => "relates")
874 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
874 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
875 :issue_to => second_issue,
875 :issue_to => second_issue,
876 :relation_type => "duplicates")
876 :relation_type => "duplicates")
877
877
878 assert @project.copy(@source_project)
878 assert @project.copy(@source_project)
879 assert_equal @source_project.issues.count, @project.issues.count
879 assert_equal @source_project.issues.count, @project.issues.count
880 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
880 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
881 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
881 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
882
882
883 # First issue with a relation on project
883 # First issue with a relation on project
884 assert_equal 1, copied_issue.relations.size, "Relation not copied"
884 assert_equal 1, copied_issue.relations.size, "Relation not copied"
885 copied_relation = copied_issue.relations.first
885 copied_relation = copied_issue.relations.first
886 assert_equal "relates", copied_relation.relation_type
886 assert_equal "relates", copied_relation.relation_type
887 assert_equal copied_second_issue.id, copied_relation.issue_to_id
887 assert_equal copied_second_issue.id, copied_relation.issue_to_id
888 assert_not_equal source_relation.id, copied_relation.id
888 assert_not_equal source_relation.id, copied_relation.id
889
889
890 # Second issue with a cross project relation
890 # Second issue with a cross project relation
891 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
891 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
892 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
892 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
893 assert_equal "duplicates", copied_relation.relation_type
893 assert_equal "duplicates", copied_relation.relation_type
894 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
894 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
895 assert_not_equal source_relation_cross_project.id, copied_relation.id
895 assert_not_equal source_relation_cross_project.id, copied_relation.id
896 end
896 end
897
897
898 should "copy issue attachments" do
898 should "copy issue attachments" do
899 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
899 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
900 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
900 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
901 @source_project.issues << issue
901 @source_project.issues << issue
902 assert @project.copy(@source_project)
902 assert @project.copy(@source_project)
903
903
904 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
904 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
905 assert_not_nil copied_issue
905 assert_not_nil copied_issue
906 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
906 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
907 assert_equal "testfile.txt", copied_issue.attachments.first.filename
907 assert_equal "testfile.txt", copied_issue.attachments.first.filename
908 end
908 end
909
909
910 should "copy memberships" do
910 should "copy memberships" do
911 assert @project.valid?
911 assert @project.valid?
912 assert @project.members.empty?
912 assert @project.members.empty?
913 assert @project.copy(@source_project)
913 assert @project.copy(@source_project)
914
914
915 assert_equal @source_project.memberships.size, @project.memberships.size
915 assert_equal @source_project.memberships.size, @project.memberships.size
916 @project.memberships.each do |membership|
916 @project.memberships.each do |membership|
917 assert membership
917 assert membership
918 assert_equal @project, membership.project
918 assert_equal @project, membership.project
919 end
919 end
920 end
920 end
921
921
922 should "copy memberships with groups and additional roles" do
922 should "copy memberships with groups and additional roles" do
923 group = Group.create!(:lastname => "Copy group")
923 group = Group.create!(:lastname => "Copy group")
924 user = User.find(7)
924 user = User.find(7)
925 group.users << user
925 group.users << user
926 # group role
926 # group role
927 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
927 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
928 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
928 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
929 # additional role
929 # additional role
930 member.role_ids = [1]
930 member.role_ids = [1]
931
931
932 assert @project.copy(@source_project)
932 assert @project.copy(@source_project)
933 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
933 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
934 assert_not_nil member
934 assert_not_nil member
935 assert_equal [1, 2], member.role_ids.sort
935 assert_equal [1, 2], member.role_ids.sort
936 end
936 end
937
937
938 should "copy project specific queries" do
938 should "copy project specific queries" do
939 assert @project.valid?
939 assert @project.valid?
940 assert @project.queries.empty?
940 assert @project.queries.empty?
941 assert @project.copy(@source_project)
941 assert @project.copy(@source_project)
942
942
943 assert_equal @source_project.queries.size, @project.queries.size
943 assert_equal @source_project.queries.size, @project.queries.size
944 @project.queries.each do |query|
944 @project.queries.each do |query|
945 assert query
945 assert query
946 assert_equal @project, query.project
946 assert_equal @project, query.project
947 end
947 end
948 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
948 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
949 end
949 end
950
950
951 should "copy versions" do
951 should "copy versions" do
952 @source_project.versions << Version.generate!
952 @source_project.versions << Version.generate!
953 @source_project.versions << Version.generate!
953 @source_project.versions << Version.generate!
954
954
955 assert @project.versions.empty?
955 assert @project.versions.empty?
956 assert @project.copy(@source_project)
956 assert @project.copy(@source_project)
957
957
958 assert_equal @source_project.versions.size, @project.versions.size
958 assert_equal @source_project.versions.size, @project.versions.size
959 @project.versions.each do |version|
959 @project.versions.each do |version|
960 assert version
960 assert version
961 assert_equal @project, version.project
961 assert_equal @project, version.project
962 end
962 end
963 end
963 end
964
964
965 should "copy wiki" do
965 should "copy wiki" do
966 assert_difference 'Wiki.count' do
966 assert_difference 'Wiki.count' do
967 assert @project.copy(@source_project)
967 assert @project.copy(@source_project)
968 end
968 end
969
969
970 assert @project.wiki
970 assert @project.wiki
971 assert_not_equal @source_project.wiki, @project.wiki
971 assert_not_equal @source_project.wiki, @project.wiki
972 assert_equal "Start page", @project.wiki.start_page
972 assert_equal "Start page", @project.wiki.start_page
973 end
973 end
974
974
975 should "copy wiki pages and content with hierarchy" do
975 should "copy wiki pages and content with hierarchy" do
976 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
976 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
977 assert @project.copy(@source_project)
977 assert @project.copy(@source_project)
978 end
978 end
979
979
980 assert @project.wiki
980 assert @project.wiki
981 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
981 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
982
982
983 @project.wiki.pages.each do |wiki_page|
983 @project.wiki.pages.each do |wiki_page|
984 assert wiki_page.content
984 assert wiki_page.content
985 assert !@source_project.wiki.pages.include?(wiki_page)
985 assert !@source_project.wiki.pages.include?(wiki_page)
986 end
986 end
987
987
988 parent = @project.wiki.find_page('Parent_page')
988 parent = @project.wiki.find_page('Parent_page')
989 child1 = @project.wiki.find_page('Child_page_1')
989 child1 = @project.wiki.find_page('Child_page_1')
990 child2 = @project.wiki.find_page('Child_page_2')
990 child2 = @project.wiki.find_page('Child_page_2')
991 assert_equal parent, child1.parent
991 assert_equal parent, child1.parent
992 assert_equal parent, child2.parent
992 assert_equal parent, child2.parent
993 end
993 end
994
994
995 should "copy issue categories" do
995 should "copy issue categories" do
996 assert @project.copy(@source_project)
996 assert @project.copy(@source_project)
997
997
998 assert_equal 2, @project.issue_categories.size
998 assert_equal 2, @project.issue_categories.size
999 @project.issue_categories.each do |issue_category|
999 @project.issue_categories.each do |issue_category|
1000 assert !@source_project.issue_categories.include?(issue_category)
1000 assert !@source_project.issue_categories.include?(issue_category)
1001 end
1001 end
1002 end
1002 end
1003
1003
1004 should "copy boards" do
1004 should "copy boards" do
1005 assert @project.copy(@source_project)
1005 assert @project.copy(@source_project)
1006
1006
1007 assert_equal 1, @project.boards.size
1007 assert_equal 1, @project.boards.size
1008 @project.boards.each do |board|
1008 @project.boards.each do |board|
1009 assert !@source_project.boards.include?(board)
1009 assert !@source_project.boards.include?(board)
1010 end
1010 end
1011 end
1011 end
1012
1012
1013 should "change the new issues to use the copied issue categories" do
1013 should "change the new issues to use the copied issue categories" do
1014 issue = Issue.find(4)
1014 issue = Issue.find(4)
1015 issue.update_attribute(:category_id, 3)
1015 issue.update_attribute(:category_id, 3)
1016
1016
1017 assert @project.copy(@source_project)
1017 assert @project.copy(@source_project)
1018
1018
1019 @project.issues.each do |issue|
1019 @project.issues.each do |issue|
1020 assert issue.category
1020 assert issue.category
1021 assert_equal "Stock management", issue.category.name # Same name
1021 assert_equal "Stock management", issue.category.name # Same name
1022 assert_not_equal IssueCategory.find(3), issue.category # Different record
1022 assert_not_equal IssueCategory.find(3), issue.category # Different record
1023 end
1023 end
1024 end
1024 end
1025
1025
1026 should "limit copy with :only option" do
1026 should "limit copy with :only option" do
1027 assert @project.members.empty?
1027 assert @project.members.empty?
1028 assert @project.issue_categories.empty?
1028 assert @project.issue_categories.empty?
1029 assert @source_project.issues.any?
1029 assert @source_project.issues.any?
1030
1030
1031 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
1031 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
1032
1032
1033 assert @project.members.any?
1033 assert @project.members.any?
1034 assert @project.issue_categories.any?
1034 assert @project.issue_categories.any?
1035 assert @project.issues.empty?
1035 assert @project.issues.empty?
1036 end
1036 end
1037 end
1037 end
1038
1038
1039 def test_copy_should_copy_subtasks
1039 def test_copy_should_copy_subtasks
1040 source = Project.generate!(:tracker_ids => [1])
1040 source = Project.generate!(:tracker_ids => [1])
1041 issue = Issue.generate_with_descendants!(:project => source)
1041 issue = Issue.generate_with_descendants!(:project => source)
1042 project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1])
1042 project = Project.new(:name => 'Copy', :identifier => 'copy', :tracker_ids => [1])
1043
1043
1044 assert_difference 'Project.count' do
1044 assert_difference 'Project.count' do
1045 assert_difference 'Issue.count', 1+issue.descendants.count do
1045 assert_difference 'Issue.count', 1+issue.descendants.count do
1046 assert project.copy(source.reload)
1046 assert project.copy(source.reload)
1047 end
1047 end
1048 end
1048 end
1049 copy = Issue.where(:parent_id => nil).order("id DESC").first
1049 copy = Issue.where(:parent_id => nil).order("id DESC").first
1050 assert_equal project, copy.project
1050 assert_equal project, copy.project
1051 assert_equal issue.descendants.count, copy.descendants.count
1051 assert_equal issue.descendants.count, copy.descendants.count
1052 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1052 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
1053 assert child_copy.descendants.any?
1053 assert child_copy.descendants.any?
1054 end
1054 end
1055
1055
1056 context "#start_date" do
1056 context "#start_date" do
1057 setup do
1057 setup do
1058 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1058 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1059 @project = Project.generate!(:identifier => 'test0')
1059 @project = Project.generate!(:identifier => 'test0')
1060 @project.trackers << Tracker.generate!
1060 @project.trackers << Tracker.generate!
1061 end
1061 end
1062
1062
1063 should "be nil if there are no issues on the project" do
1063 should "be nil if there are no issues on the project" do
1064 assert_nil @project.start_date
1064 assert_nil @project.start_date
1065 end
1065 end
1066
1066
1067 should "be tested when issues have no start date"
1067 should "be tested when issues have no start date"
1068
1068
1069 should "be the earliest start date of it's issues" do
1069 should "be the earliest start date of it's issues" do
1070 early = 7.days.ago.to_date
1070 early = 7.days.ago.to_date
1071 Issue.generate!(:project => @project, :start_date => Date.today)
1071 Issue.generate!(:project => @project, :start_date => Date.today)
1072 Issue.generate!(:project => @project, :start_date => early)
1072 Issue.generate!(:project => @project, :start_date => early)
1073
1073
1074 assert_equal early, @project.start_date
1074 assert_equal early, @project.start_date
1075 end
1075 end
1076
1076
1077 end
1077 end
1078
1078
1079 context "#due_date" do
1079 context "#due_date" do
1080 setup do
1080 setup do
1081 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1081 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1082 @project = Project.generate!(:identifier => 'test0')
1082 @project = Project.generate!(:identifier => 'test0')
1083 @project.trackers << Tracker.generate!
1083 @project.trackers << Tracker.generate!
1084 end
1084 end
1085
1085
1086 should "be nil if there are no issues on the project" do
1086 should "be nil if there are no issues on the project" do
1087 assert_nil @project.due_date
1087 assert_nil @project.due_date
1088 end
1088 end
1089
1089
1090 should "be tested when issues have no due date"
1090 should "be tested when issues have no due date"
1091
1091
1092 should "be the latest due date of it's issues" do
1092 should "be the latest due date of it's issues" do
1093 future = 7.days.from_now.to_date
1093 future = 7.days.from_now.to_date
1094 Issue.generate!(:project => @project, :due_date => future)
1094 Issue.generate!(:project => @project, :due_date => future)
1095 Issue.generate!(:project => @project, :due_date => Date.today)
1095 Issue.generate!(:project => @project, :due_date => Date.today)
1096
1096
1097 assert_equal future, @project.due_date
1097 assert_equal future, @project.due_date
1098 end
1098 end
1099
1099
1100 should "be the latest due date of it's versions" do
1100 should "be the latest due date of it's versions" do
1101 future = 7.days.from_now.to_date
1101 future = 7.days.from_now.to_date
1102 @project.versions << Version.generate!(:effective_date => future)
1102 @project.versions << Version.generate!(:effective_date => future)
1103 @project.versions << Version.generate!(:effective_date => Date.today)
1103 @project.versions << Version.generate!(:effective_date => Date.today)
1104
1104
1105
1105
1106 assert_equal future, @project.due_date
1106 assert_equal future, @project.due_date
1107
1107
1108 end
1108 end
1109
1109
1110 should "pick the latest date from it's issues and versions" do
1110 should "pick the latest date from it's issues and versions" do
1111 future = 7.days.from_now.to_date
1111 future = 7.days.from_now.to_date
1112 far_future = 14.days.from_now.to_date
1112 far_future = 14.days.from_now.to_date
1113 Issue.generate!(:project => @project, :due_date => far_future)
1113 Issue.generate!(:project => @project, :due_date => far_future)
1114 @project.versions << Version.generate!(:effective_date => future)
1114 @project.versions << Version.generate!(:effective_date => future)
1115
1115
1116 assert_equal far_future, @project.due_date
1116 assert_equal far_future, @project.due_date
1117 end
1117 end
1118
1118
1119 end
1119 end
1120
1120
1121 context "Project#completed_percent" do
1121 context "Project#completed_percent" do
1122 setup do
1122 setup do
1123 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1123 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1124 @project = Project.generate!(:identifier => 'test0')
1124 @project = Project.generate!(:identifier => 'test0')
1125 @project.trackers << Tracker.generate!
1125 @project.trackers << Tracker.generate!
1126 end
1126 end
1127
1127
1128 context "no versions" do
1128 context "no versions" do
1129 should "be 100" do
1129 should "be 100" do
1130 assert_equal 100, @project.completed_percent
1130 assert_equal 100, @project.completed_percent
1131 end
1131 end
1132 end
1132 end
1133
1133
1134 context "with versions" do
1134 context "with versions" do
1135 should "return 0 if the versions have no issues" do
1135 should "return 0 if the versions have no issues" do
1136 Version.generate!(:project => @project)
1136 Version.generate!(:project => @project)
1137 Version.generate!(:project => @project)
1137 Version.generate!(:project => @project)
1138
1138
1139 assert_equal 0, @project.completed_percent
1139 assert_equal 0, @project.completed_percent
1140 end
1140 end
1141
1141
1142 should "return 100 if the version has only closed issues" do
1142 should "return 100 if the version has only closed issues" do
1143 v1 = Version.generate!(:project => @project)
1143 v1 = Version.generate!(:project => @project)
1144 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1144 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1145 v2 = Version.generate!(:project => @project)
1145 v2 = Version.generate!(:project => @project)
1146 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1146 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1147
1147
1148 assert_equal 100, @project.completed_percent
1148 assert_equal 100, @project.completed_percent
1149 end
1149 end
1150
1150
1151 should "return the averaged completed percent of the versions (not weighted)" do
1151 should "return the averaged completed percent of the versions (not weighted)" do
1152 v1 = Version.generate!(:project => @project)
1152 v1 = Version.generate!(:project => @project)
1153 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1153 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1154 v2 = Version.generate!(:project => @project)
1154 v2 = Version.generate!(:project => @project)
1155 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1155 Issue.generate!(:project => @project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1156
1156
1157 assert_equal 50, @project.completed_percent
1157 assert_equal 50, @project.completed_percent
1158 end
1158 end
1159
1159
1160 end
1160 end
1161 end
1161 end
1162
1162
1163 context "#notified_users" do
1163 context "#notified_users" do
1164 setup do
1164 setup do
1165 @project = Project.generate!
1165 @project = Project.generate!
1166 @role = Role.generate!
1166 @role = Role.generate!
1167
1167
1168 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1168 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1169 Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1169 Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1170
1170
1171 @all_events_user = User.generate!(:mail_notification => 'all')
1171 @all_events_user = User.generate!(:mail_notification => 'all')
1172 Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user)
1172 Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user)
1173
1173
1174 @no_events_user = User.generate!(:mail_notification => 'none')
1174 @no_events_user = User.generate!(:mail_notification => 'none')
1175 Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user)
1175 Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user)
1176
1176
1177 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1177 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1178 Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1178 Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1179
1179
1180 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1180 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1181 Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1181 Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1182
1182
1183 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1183 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1184 Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1184 Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1185 end
1185 end
1186
1186
1187 should "include members with a mail notification" do
1187 should "include members with a mail notification" do
1188 assert @project.notified_users.include?(@user_with_membership_notification)
1188 assert @project.notified_users.include?(@user_with_membership_notification)
1189 end
1189 end
1190
1190
1191 should "include users with the 'all' notification option" do
1191 should "include users with the 'all' notification option" do
1192 assert @project.notified_users.include?(@all_events_user)
1192 assert @project.notified_users.include?(@all_events_user)
1193 end
1193 end
1194
1194
1195 should "not include users with the 'none' notification option" do
1195 should "not include users with the 'none' notification option" do
1196 assert !@project.notified_users.include?(@no_events_user)
1196 assert !@project.notified_users.include?(@no_events_user)
1197 end
1197 end
1198
1198
1199 should "not include users with the 'only_my_events' notification option" do
1199 should "not include users with the 'only_my_events' notification option" do
1200 assert !@project.notified_users.include?(@only_my_events_user)
1200 assert !@project.notified_users.include?(@only_my_events_user)
1201 end
1201 end
1202
1202
1203 should "not include users with the 'only_assigned' notification option" do
1203 should "not include users with the 'only_assigned' notification option" do
1204 assert !@project.notified_users.include?(@only_assigned_user)
1204 assert !@project.notified_users.include?(@only_assigned_user)
1205 end
1205 end
1206
1206
1207 should "not include users with the 'only_owner' notification option" do
1207 should "not include users with the 'only_owner' notification option" do
1208 assert !@project.notified_users.include?(@only_owned_user)
1208 assert !@project.notified_users.include?(@only_owned_user)
1209 end
1209 end
1210 end
1210 end
1211
1211
1212 end
1212 end
@@ -1,306 +1,306
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositoryBazaarTest < ActiveSupport::TestCase
20 class RepositoryBazaarTest < ActiveSupport::TestCase
21 fixtures :projects
21 fixtures :projects
22
22
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s
25 REPOSITORY_PATH = Rails.root.join('tmp/test/bazaar_repository').to_s
26 REPOSITORY_PATH_TRUNK = File.join(REPOSITORY_PATH, "trunk")
26 REPOSITORY_PATH_TRUNK = File.join(REPOSITORY_PATH, "trunk")
27 NUM_REV = 4
27 NUM_REV = 4
28
28
29 REPOSITORY_PATH_NON_ASCII = Rails.root.join(REPOSITORY_PATH + '/' + 'non_ascii').to_s
29 REPOSITORY_PATH_NON_ASCII = Rails.root.join(REPOSITORY_PATH + '/' + 'non_ascii').to_s
30
30
31 # Bazaar core does not support xml output such as Subversion and Mercurial.
31 # Bazaar core does not support xml output such as Subversion and Mercurial.
32 # "bzr" command output and command line parameter depend on locale.
32 # "bzr" command output and command line parameter depend on locale.
33 # So, non ASCII path tests cannot run independent locale.
33 # So, non ASCII path tests cannot run independent locale.
34 #
34 #
35 # If you want to run Bazaar non ASCII path tests on Linux *Ruby 1.9*,
35 # If you want to run Bazaar non ASCII path tests on Linux *Ruby 1.9*,
36 # you need to set locale character set "ISO-8859-1".
36 # you need to set locale character set "ISO-8859-1".
37 # E.g. "LANG=en_US.ISO-8859-1".
37 # E.g. "LANG=en_US.ISO-8859-1".
38 # On Linux other platforms (e.g. Ruby 1.8, JRuby),
38 # On Linux other platforms (e.g. Ruby 1.8, JRuby),
39 # you need to set "RUN_LATIN1_OUTPUT_TEST = true" manually.
39 # you need to set "RUN_LATIN1_OUTPUT_TEST = true" manually.
40 #
40 #
41 # On Windows, because it is too hard to change system locale,
41 # On Windows, because it is too hard to change system locale,
42 # you cannot run Bazaar non ASCII path tests.
42 # you cannot run Bazaar non ASCII path tests.
43 #
43 #
44 RUN_LATIN1_OUTPUT_TEST = (RUBY_PLATFORM != 'java' &&
44 RUN_LATIN1_OUTPUT_TEST = (RUBY_PLATFORM != 'java' &&
45 REPOSITORY_PATH.respond_to?(:force_encoding) &&
45 REPOSITORY_PATH.respond_to?(:force_encoding) &&
46 Encoding.locale_charmap == "ISO-8859-1")
46 Encoding.locale_charmap == "ISO-8859-1")
47
47
48 CHAR_1_UTF8_HEX = "\xc3\x9c"
48 CHAR_1_UTF8_HEX = "\xc3\x9c"
49 CHAR_1_LATIN1_HEX = "\xdc"
49 CHAR_1_LATIN1_HEX = "\xdc"
50
50
51 def setup
51 def setup
52 @project = Project.find(3)
52 @project = Project.find(3)
53 @repository = Repository::Bazaar.create(
53 @repository = Repository::Bazaar.create(
54 :project => @project, :url => REPOSITORY_PATH_TRUNK,
54 :project => @project, :url => REPOSITORY_PATH_TRUNK,
55 :log_encoding => 'UTF-8')
55 :log_encoding => 'UTF-8')
56 assert @repository
56 assert @repository
57 @char_1_utf8 = CHAR_1_UTF8_HEX.dup
57 @char_1_utf8 = CHAR_1_UTF8_HEX.dup
58 @char_1_ascii8bit = CHAR_1_LATIN1_HEX.dup
58 @char_1_ascii8bit = CHAR_1_LATIN1_HEX.dup
59 if @char_1_utf8.respond_to?(:force_encoding)
59 if @char_1_utf8.respond_to?(:force_encoding)
60 @char_1_utf8.force_encoding('UTF-8')
60 @char_1_utf8.force_encoding('UTF-8')
61 @char_1_ascii8bit.force_encoding('ASCII-8BIT')
61 @char_1_ascii8bit.force_encoding('ASCII-8BIT')
62 end
62 end
63 end
63 end
64
64
65 def test_blank_path_to_repository_error_message
65 def test_blank_path_to_repository_error_message
66 set_language_if_valid 'en'
66 set_language_if_valid 'en'
67 repo = Repository::Bazaar.new(
67 repo = Repository::Bazaar.new(
68 :project => @project,
68 :project => @project,
69 :identifier => 'test',
69 :identifier => 'test',
70 :log_encoding => 'UTF-8'
70 :log_encoding => 'UTF-8'
71 )
71 )
72 assert !repo.save
72 assert !repo.save
73 assert_include "Path to repository can't be blank",
73 assert_include "Path to repository can't be blank",
74 repo.errors.full_messages
74 repo.errors.full_messages
75 end
75 end
76
76
77 def test_blank_path_to_repository_error_message_fr
77 def test_blank_path_to_repository_error_message_fr
78 set_language_if_valid 'fr'
78 set_language_if_valid 'fr'
79 str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)"
79 str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)"
80 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
80 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
81 repo = Repository::Bazaar.new(
81 repo = Repository::Bazaar.new(
82 :project => @project,
82 :project => @project,
83 :url => "",
83 :url => "",
84 :identifier => 'test',
84 :identifier => 'test',
85 :log_encoding => 'UTF-8'
85 :log_encoding => 'UTF-8'
86 )
86 )
87 assert !repo.save
87 assert !repo.save
88 assert_include str, repo.errors.full_messages
88 assert_include str, repo.errors.full_messages
89 end
89 end
90
90
91 if File.directory?(REPOSITORY_PATH_TRUNK)
91 if File.directory?(REPOSITORY_PATH_TRUNK)
92 def test_fetch_changesets_from_scratch
92 def test_fetch_changesets_from_scratch
93 assert_equal 0, @repository.changesets.count
93 assert_equal 0, @repository.changesets.count
94 @repository.fetch_changesets
94 @repository.fetch_changesets
95 @project.reload
95 @project.reload
96
96
97 assert_equal NUM_REV, @repository.changesets.count
97 assert_equal NUM_REV, @repository.changesets.count
98 assert_equal 9, @repository.filechanges.count
98 assert_equal 9, @repository.filechanges.count
99 assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments
99 assert_equal 'Initial import', @repository.changesets.find_by_revision('1').comments
100 end
100 end
101
101
102 def test_fetch_changesets_incremental
102 def test_fetch_changesets_incremental
103 assert_equal 0, @repository.changesets.count
103 assert_equal 0, @repository.changesets.count
104 @repository.fetch_changesets
104 @repository.fetch_changesets
105 @project.reload
105 @project.reload
106 assert_equal NUM_REV, @repository.changesets.count
106 assert_equal NUM_REV, @repository.changesets.count
107 # Remove changesets with revision > 5
107 # Remove changesets with revision > 5
108 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
108 @repository.changesets.all.each {|c| c.destroy if c.revision.to_i > 2}
109 @project.reload
109 @project.reload
110 assert_equal 2, @repository.changesets.count
110 assert_equal 2, @repository.changesets.count
111
111
112 @repository.fetch_changesets
112 @repository.fetch_changesets
113 @project.reload
113 @project.reload
114 assert_equal NUM_REV, @repository.changesets.count
114 assert_equal NUM_REV, @repository.changesets.count
115 end
115 end
116
116
117 def test_entries
117 def test_entries
118 entries = @repository.entries
118 entries = @repository.entries
119 assert_kind_of Redmine::Scm::Adapters::Entries, entries
119 assert_kind_of Redmine::Scm::Adapters::Entries, entries
120 assert_equal 2, entries.size
120 assert_equal 2, entries.size
121
121
122 assert_equal 'dir', entries[0].kind
122 assert_equal 'dir', entries[0].kind
123 assert_equal 'directory', entries[0].name
123 assert_equal 'directory', entries[0].name
124 assert_equal 'directory', entries[0].path
124 assert_equal 'directory', entries[0].path
125
125
126 assert_equal 'file', entries[1].kind
126 assert_equal 'file', entries[1].kind
127 assert_equal 'doc-mkdir.txt', entries[1].name
127 assert_equal 'doc-mkdir.txt', entries[1].name
128 assert_equal 'doc-mkdir.txt', entries[1].path
128 assert_equal 'doc-mkdir.txt', entries[1].path
129 end
129 end
130
130
131 def test_entries_in_subdirectory
131 def test_entries_in_subdirectory
132 entries = @repository.entries('directory')
132 entries = @repository.entries('directory')
133 assert_equal 3, entries.size
133 assert_equal 3, entries.size
134
134
135 assert_equal 'file', entries.last.kind
135 assert_equal 'file', entries.last.kind
136 assert_equal 'edit.png', entries.last.name
136 assert_equal 'edit.png', entries.last.name
137 assert_equal 'directory/edit.png', entries.last.path
137 assert_equal 'directory/edit.png', entries.last.path
138 end
138 end
139
139
140 def test_previous
140 def test_previous
141 assert_equal 0, @repository.changesets.count
141 assert_equal 0, @repository.changesets.count
142 @repository.fetch_changesets
142 @repository.fetch_changesets
143 @project.reload
143 @project.reload
144 assert_equal NUM_REV, @repository.changesets.count
144 assert_equal NUM_REV, @repository.changesets.count
145 changeset = @repository.find_changeset_by_name('3')
145 changeset = @repository.find_changeset_by_name('3')
146 assert_equal @repository.find_changeset_by_name('2'), changeset.previous
146 assert_equal @repository.find_changeset_by_name('2'), changeset.previous
147 end
147 end
148
148
149 def test_previous_nil
149 def test_previous_nil
150 assert_equal 0, @repository.changesets.count
150 assert_equal 0, @repository.changesets.count
151 @repository.fetch_changesets
151 @repository.fetch_changesets
152 @project.reload
152 @project.reload
153 assert_equal NUM_REV, @repository.changesets.count
153 assert_equal NUM_REV, @repository.changesets.count
154 changeset = @repository.find_changeset_by_name('1')
154 changeset = @repository.find_changeset_by_name('1')
155 assert_nil changeset.previous
155 assert_nil changeset.previous
156 end
156 end
157
157
158 def test_next
158 def test_next
159 assert_equal 0, @repository.changesets.count
159 assert_equal 0, @repository.changesets.count
160 @repository.fetch_changesets
160 @repository.fetch_changesets
161 @project.reload
161 @project.reload
162 assert_equal NUM_REV, @repository.changesets.count
162 assert_equal NUM_REV, @repository.changesets.count
163 changeset = @repository.find_changeset_by_name('2')
163 changeset = @repository.find_changeset_by_name('2')
164 assert_equal @repository.find_changeset_by_name('3'), changeset.next
164 assert_equal @repository.find_changeset_by_name('3'), changeset.next
165 end
165 end
166
166
167 def test_next_nil
167 def test_next_nil
168 assert_equal 0, @repository.changesets.count
168 assert_equal 0, @repository.changesets.count
169 @repository.fetch_changesets
169 @repository.fetch_changesets
170 @project.reload
170 @project.reload
171 assert_equal NUM_REV, @repository.changesets.count
171 assert_equal NUM_REV, @repository.changesets.count
172 changeset = @repository.find_changeset_by_name('4')
172 changeset = @repository.find_changeset_by_name('4')
173 assert_nil changeset.next
173 assert_nil changeset.next
174 end
174 end
175
175
176 if File.directory?(REPOSITORY_PATH_NON_ASCII) && RUN_LATIN1_OUTPUT_TEST
176 if File.directory?(REPOSITORY_PATH_NON_ASCII) && RUN_LATIN1_OUTPUT_TEST
177 def test_cat_latin1_path
177 def test_cat_latin1_path
178 latin1_repo = create_latin1_repo
178 latin1_repo = create_latin1_repo
179 buf = latin1_repo.cat(
179 buf = latin1_repo.cat(
180 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2)
180 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2)
181 assert buf
181 assert buf
182 lines = buf.split("\n")
182 lines = buf.split("\n")
183 assert_equal 2, lines.length
183 assert_equal 2, lines.length
184 assert_equal 'It is written in Python.', lines[1]
184 assert_equal 'It is written in Python.', lines[1]
185
185
186 buf = latin1_repo.cat(
186 buf = latin1_repo.cat(
187 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2)
187 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2)
188 assert buf
188 assert buf
189 lines = buf.split("\n")
189 lines = buf.split("\n")
190 assert_equal 1, lines.length
190 assert_equal 1, lines.length
191 assert_equal "test-#{@char_1_ascii8bit}.txt", lines[0]
191 assert_equal "test-#{@char_1_ascii8bit}.txt", lines[0]
192 end
192 end
193
193
194 def test_annotate_latin1_path
194 def test_annotate_latin1_path
195 latin1_repo = create_latin1_repo
195 latin1_repo = create_latin1_repo
196 ann1 = latin1_repo.annotate(
196 ann1 = latin1_repo.annotate(
197 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2)
197 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", 2)
198 assert_equal 2, ann1.lines.size
198 assert_equal 2, ann1.lines.size
199 assert_equal '2', ann1.revisions[0].identifier
199 assert_equal '2', ann1.revisions[0].identifier
200 assert_equal 'test00@', ann1.revisions[0].author
200 assert_equal 'test00@', ann1.revisions[0].author
201 assert_equal 'It is written in Python.', ann1.lines[1]
201 assert_equal 'It is written in Python.', ann1.lines[1]
202 ann2 = latin1_repo.annotate(
202 ann2 = latin1_repo.annotate(
203 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2)
203 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2)
204 assert_equal 1, ann2.lines.size
204 assert_equal 1, ann2.lines.size
205 assert_equal '2', ann2.revisions[0].identifier
205 assert_equal '2', ann2.revisions[0].identifier
206 assert_equal 'test00@', ann2.revisions[0].author
206 assert_equal 'test00@', ann2.revisions[0].author
207 assert_equal "test-#{@char_1_ascii8bit}.txt", ann2.lines[0]
207 assert_equal "test-#{@char_1_ascii8bit}.txt", ann2.lines[0]
208 end
208 end
209
209
210 def test_diff_latin1_path
210 def test_diff_latin1_path
211 latin1_repo = create_latin1_repo
211 latin1_repo = create_latin1_repo
212 diff1 = latin1_repo.diff(
212 diff1 = latin1_repo.diff(
213 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2, 1)
213 "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", 2, 1)
214 assert_equal 7, diff1.size
214 assert_equal 7, diff1.size
215 buf = diff1[5].gsub(/\r\n|\r|\n/, "")
215 buf = diff1[5].gsub(/\r\n|\r|\n/, "")
216 assert_equal "+test-#{@char_1_ascii8bit}.txt", buf
216 assert_equal "+test-#{@char_1_ascii8bit}.txt", buf
217 end
217 end
218
218
219 def test_entries_latin1_path
219 def test_entries_latin1_path
220 latin1_repo = create_latin1_repo
220 latin1_repo = create_latin1_repo
221 entries = latin1_repo.entries("test-#{@char_1_utf8}-dir", 2)
221 entries = latin1_repo.entries("test-#{@char_1_utf8}-dir", 2)
222 assert_kind_of Redmine::Scm::Adapters::Entries, entries
222 assert_kind_of Redmine::Scm::Adapters::Entries, entries
223 assert_equal 3, entries.size
223 assert_equal 3, entries.size
224 assert_equal 'file', entries[1].kind
224 assert_equal 'file', entries[1].kind
225 assert_equal "test-#{@char_1_utf8}-1.txt", entries[0].name
225 assert_equal "test-#{@char_1_utf8}-1.txt", entries[0].name
226 assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", entries[0].path
226 assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", entries[0].path
227 end
227 end
228
228
229 def test_entry_latin1_path
229 def test_entry_latin1_path
230 latin1_repo = create_latin1_repo
230 latin1_repo = create_latin1_repo
231 ["test-#{@char_1_utf8}-dir",
231 ["test-#{@char_1_utf8}-dir",
232 "/test-#{@char_1_utf8}-dir",
232 "/test-#{@char_1_utf8}-dir",
233 "/test-#{@char_1_utf8}-dir/"
233 "/test-#{@char_1_utf8}-dir/"
234 ].each do |path|
234 ].each do |path|
235 entry = latin1_repo.entry(path, 2)
235 entry = latin1_repo.entry(path, 2)
236 assert_equal "test-#{@char_1_utf8}-dir", entry.path
236 assert_equal "test-#{@char_1_utf8}-dir", entry.path
237 assert_equal "dir", entry.kind
237 assert_equal "dir", entry.kind
238 end
238 end
239 ["test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt",
239 ["test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt",
240 "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt"
240 "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt"
241 ].each do |path|
241 ].each do |path|
242 entry = latin1_repo.entry(path, 2)
242 entry = latin1_repo.entry(path, 2)
243 assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt",
243 assert_equal "test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt",
244 entry.path
244 entry.path
245 assert_equal "file", entry.kind
245 assert_equal "file", entry.kind
246 end
246 end
247 end
247 end
248
248
249 def test_changeset_latin1_path
249 def test_changeset_latin1_path
250 latin1_repo = create_latin1_repo
250 latin1_repo = create_latin1_repo
251 assert_equal 0, latin1_repo.changesets.count
251 assert_equal 0, latin1_repo.changesets.count
252 latin1_repo.fetch_changesets
252 latin1_repo.fetch_changesets
253 @project.reload
253 @project.reload
254 assert_equal 3, latin1_repo.changesets.count
254 assert_equal 3, latin1_repo.changesets.count
255
255
256 cs2 = latin1_repo.changesets.find_by_revision('2')
256 cs2 = latin1_repo.changesets.find_by_revision('2')
257 assert_not_nil cs2
257 assert_not_nil cs2
258 assert_equal "test-#{@char_1_utf8}", cs2.comments
258 assert_equal "test-#{@char_1_utf8}", cs2.comments
259 c2 = cs2.filechanges.sort_by(&:path)
259 c2 = cs2.filechanges.sort_by(&:path)
260 assert_equal 4, c2.size
260 assert_equal 4, c2.size
261 assert_equal 'A', c2[0].action
261 assert_equal 'A', c2[0].action
262 assert_equal "/test-#{@char_1_utf8}-dir/", c2[0].path
262 assert_equal "/test-#{@char_1_utf8}-dir/", c2[0].path
263 assert_equal 'A', c2[1].action
263 assert_equal 'A', c2[1].action
264 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", c2[1].path
264 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-1.txt", c2[1].path
265 assert_equal 'A', c2[2].action
265 assert_equal 'A', c2[2].action
266 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c2[2].path
266 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c2[2].path
267 assert_equal 'A', c2[3].action
267 assert_equal 'A', c2[3].action
268 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c2[3].path
268 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c2[3].path
269
269
270 cs3 = latin1_repo.changesets.find_by_revision('3')
270 cs3 = latin1_repo.changesets.find_by_revision('3')
271 assert_not_nil cs3
271 assert_not_nil cs3
272 assert_equal "modify, move and delete #{@char_1_utf8} files", cs3.comments
272 assert_equal "modify, move and delete #{@char_1_utf8} files", cs3.comments
273 c3 = cs3.filechanges.sort_by(&:path)
273 c3 = cs3.filechanges.sort_by(&:path)
274 assert_equal 3, c3.size
274 assert_equal 3, c3.size
275 assert_equal 'M', c3[0].action
275 assert_equal 'M', c3[0].action
276 assert_equal "/test-#{@char_1_utf8}-1.txt", c3[0].path
276 assert_equal "/test-#{@char_1_utf8}-1.txt", c3[0].path
277 assert_equal 'D', c3[1].action
277 assert_equal 'D', c3[1].action
278 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c3[1].path
278 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}-2.txt", c3[1].path
279 assert_equal 'M', c3[2].action
279 assert_equal 'M', c3[2].action
280 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c3[2].path
280 assert_equal "/test-#{@char_1_utf8}-dir/test-#{@char_1_utf8}.txt", c3[2].path
281 end
281 end
282 else
282 else
283 msg = "Bazaar non ASCII output test cannot run this environment." + "\n"
283 msg = "Bazaar non ASCII output test cannot run this environment." + "\n"
284 if msg.respond_to?(:force_encoding)
284 if msg.respond_to?(:force_encoding)
285 msg += "Encoding.locale_charmap: " + Encoding.locale_charmap + "\n"
285 msg += "Encoding.locale_charmap: " + Encoding.locale_charmap + "\n"
286 end
286 end
287 puts msg
287 puts msg
288 end
288 end
289
289
290 private
290 private
291
291
292 def create_latin1_repo
292 def create_latin1_repo
293 repo = Repository::Bazaar.create(
293 repo = Repository::Bazaar.create(
294 :project => @project,
294 :project => @project,
295 :identifier => 'latin1',
295 :identifier => 'latin1',
296 :url => REPOSITORY_PATH_NON_ASCII,
296 :url => REPOSITORY_PATH_NON_ASCII,
297 :log_encoding => 'ISO-8859-1'
297 :log_encoding => 'ISO-8859-1'
298 )
298 )
299 assert repo
299 assert repo
300 repo
300 repo
301 end
301 end
302 else
302 else
303 puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!"
303 puts "Bazaar test repository NOT FOUND. Skipping unit tests !!!"
304 def test_fake; assert true end
304 def test_fake; assert true end
305 end
305 end
306 end
306 end
@@ -1,241 +1,241
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'pp'
19 require 'pp'
20 class RepositoryCvsTest < ActiveSupport::TestCase
20 class RepositoryCvsTest < ActiveSupport::TestCase
21 fixtures :projects
21 fixtures :projects
22
22
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s
25 REPOSITORY_PATH = Rails.root.join('tmp/test/cvs_repository').to_s
26 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
26 REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
27 # CVS module
27 # CVS module
28 MODULE_NAME = 'test'
28 MODULE_NAME = 'test'
29 CHANGESETS_NUM = 7
29 CHANGESETS_NUM = 7
30
30
31 def setup
31 def setup
32 @project = Project.find(3)
32 @project = Project.find(3)
33 @repository = Repository::Cvs.create(:project => @project,
33 @repository = Repository::Cvs.create(:project => @project,
34 :root_url => REPOSITORY_PATH,
34 :root_url => REPOSITORY_PATH,
35 :url => MODULE_NAME,
35 :url => MODULE_NAME,
36 :log_encoding => 'UTF-8')
36 :log_encoding => 'UTF-8')
37 assert @repository
37 assert @repository
38 end
38 end
39
39
40 def test_blank_module_error_message
40 def test_blank_module_error_message
41 set_language_if_valid 'en'
41 set_language_if_valid 'en'
42 repo = Repository::Cvs.new(
42 repo = Repository::Cvs.new(
43 :project => @project,
43 :project => @project,
44 :identifier => 'test',
44 :identifier => 'test',
45 :log_encoding => 'UTF-8',
45 :log_encoding => 'UTF-8',
46 :root_url => REPOSITORY_PATH
46 :root_url => REPOSITORY_PATH
47 )
47 )
48 assert !repo.save
48 assert !repo.save
49 assert_include "Module can't be blank",
49 assert_include "Module can't be blank",
50 repo.errors.full_messages
50 repo.errors.full_messages
51 end
51 end
52
52
53 def test_blank_module_error_message_fr
53 def test_blank_module_error_message_fr
54 set_language_if_valid 'fr'
54 set_language_if_valid 'fr'
55 str = "Module doit \xc3\xaatre renseign\xc3\xa9(e)"
55 str = "Module doit \xc3\xaatre renseign\xc3\xa9(e)"
56 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
56 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
57 repo = Repository::Cvs.new(
57 repo = Repository::Cvs.new(
58 :project => @project,
58 :project => @project,
59 :identifier => 'test',
59 :identifier => 'test',
60 :log_encoding => 'UTF-8',
60 :log_encoding => 'UTF-8',
61 :path_encoding => '',
61 :path_encoding => '',
62 :url => '',
62 :url => '',
63 :root_url => REPOSITORY_PATH
63 :root_url => REPOSITORY_PATH
64 )
64 )
65 assert !repo.save
65 assert !repo.save
66 assert_include str, repo.errors.full_messages
66 assert_include str, repo.errors.full_messages
67 end
67 end
68
68
69 def test_blank_cvsroot_error_message
69 def test_blank_cvsroot_error_message
70 set_language_if_valid 'en'
70 set_language_if_valid 'en'
71 repo = Repository::Cvs.new(
71 repo = Repository::Cvs.new(
72 :project => @project,
72 :project => @project,
73 :identifier => 'test',
73 :identifier => 'test',
74 :log_encoding => 'UTF-8',
74 :log_encoding => 'UTF-8',
75 :url => MODULE_NAME
75 :url => MODULE_NAME
76 )
76 )
77 assert !repo.save
77 assert !repo.save
78 assert_include "CVSROOT can't be blank",
78 assert_include "CVSROOT can't be blank",
79 repo.errors.full_messages
79 repo.errors.full_messages
80 end
80 end
81
81
82 def test_blank_cvsroot_error_message_fr
82 def test_blank_cvsroot_error_message_fr
83 set_language_if_valid 'fr'
83 set_language_if_valid 'fr'
84 str = "CVSROOT doit \xc3\xaatre renseign\xc3\xa9(e)"
84 str = "CVSROOT doit \xc3\xaatre renseign\xc3\xa9(e)"
85 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
85 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
86 repo = Repository::Cvs.new(
86 repo = Repository::Cvs.new(
87 :project => @project,
87 :project => @project,
88 :identifier => 'test',
88 :identifier => 'test',
89 :log_encoding => 'UTF-8',
89 :log_encoding => 'UTF-8',
90 :path_encoding => '',
90 :path_encoding => '',
91 :url => MODULE_NAME,
91 :url => MODULE_NAME,
92 :root_url => ''
92 :root_url => ''
93 )
93 )
94 assert !repo.save
94 assert !repo.save
95 assert_include str, repo.errors.full_messages
95 assert_include str, repo.errors.full_messages
96 end
96 end
97
97
98 if File.directory?(REPOSITORY_PATH)
98 if File.directory?(REPOSITORY_PATH)
99 def test_fetch_changesets_from_scratch
99 def test_fetch_changesets_from_scratch
100 assert_equal 0, @repository.changesets.count
100 assert_equal 0, @repository.changesets.count
101 @repository.fetch_changesets
101 @repository.fetch_changesets
102 @project.reload
102 @project.reload
103
103
104 assert_equal CHANGESETS_NUM, @repository.changesets.count
104 assert_equal CHANGESETS_NUM, @repository.changesets.count
105 assert_equal 16, @repository.filechanges.count
105 assert_equal 16, @repository.filechanges.count
106 assert_not_nil @repository.changesets.find_by_comments('Two files changed')
106 assert_not_nil @repository.changesets.find_by_comments('Two files changed')
107
107
108 r2 = @repository.changesets.find_by_revision('2')
108 r2 = @repository.changesets.find_by_revision('2')
109 assert_equal 'v1-20071213-162510', r2.scmid
109 assert_equal 'v1-20071213-162510', r2.scmid
110 end
110 end
111
111
112 def test_fetch_changesets_incremental
112 def test_fetch_changesets_incremental
113 assert_equal 0, @repository.changesets.count
113 assert_equal 0, @repository.changesets.count
114 @repository.fetch_changesets
114 @repository.fetch_changesets
115 @project.reload
115 @project.reload
116 assert_equal CHANGESETS_NUM, @repository.changesets.count
116 assert_equal CHANGESETS_NUM, @repository.changesets.count
117
117
118 # Remove changesets with revision > 3
118 # Remove changesets with revision > 3
119 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3}
119 @repository.changesets.all.each {|c| c.destroy if c.revision.to_i > 3}
120 @project.reload
120 @project.reload
121 assert_equal 3, @repository.changesets.count
121 assert_equal 3, @repository.changesets.count
122 assert_equal %w|3 2 1|, @repository.changesets.all.collect(&:revision)
122 assert_equal %w|3 2 1|, @repository.changesets.all.collect(&:revision)
123
123
124 rev3_commit = @repository.changesets.find(:first, :order => 'committed_on DESC')
124 rev3_commit = @repository.changesets.find(:first, :order => 'committed_on DESC')
125 assert_equal '3', rev3_commit.revision
125 assert_equal '3', rev3_commit.revision
126 # 2007-12-14 01:27:22 +0900
126 # 2007-12-14 01:27:22 +0900
127 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
127 rev3_committed_on = Time.gm(2007, 12, 13, 16, 27, 22)
128 assert_equal 'HEAD-20071213-162722', rev3_commit.scmid
128 assert_equal 'HEAD-20071213-162722', rev3_commit.scmid
129 assert_equal rev3_committed_on, rev3_commit.committed_on
129 assert_equal rev3_committed_on, rev3_commit.committed_on
130 latest_rev = @repository.latest_changeset
130 latest_rev = @repository.latest_changeset
131 assert_equal rev3_committed_on, latest_rev.committed_on
131 assert_equal rev3_committed_on, latest_rev.committed_on
132
132
133 @repository.fetch_changesets
133 @repository.fetch_changesets
134 @project.reload
134 @project.reload
135 assert_equal CHANGESETS_NUM, @repository.changesets.count
135 assert_equal CHANGESETS_NUM, @repository.changesets.count
136 assert_equal %w|7 6 5 4 3 2 1|, @repository.changesets.all.collect(&:revision)
136 assert_equal %w|7 6 5 4 3 2 1|, @repository.changesets.all.collect(&:revision)
137 rev5_commit = @repository.changesets.find_by_revision('5')
137 rev5_commit = @repository.changesets.find_by_revision('5')
138 assert_equal 'HEAD-20071213-163001', rev5_commit.scmid
138 assert_equal 'HEAD-20071213-163001', rev5_commit.scmid
139 # 2007-12-14 01:30:01 +0900
139 # 2007-12-14 01:30:01 +0900
140 rev5_committed_on = Time.gm(2007, 12, 13, 16, 30, 1)
140 rev5_committed_on = Time.gm(2007, 12, 13, 16, 30, 1)
141 assert_equal rev5_committed_on, rev5_commit.committed_on
141 assert_equal rev5_committed_on, rev5_commit.committed_on
142 end
142 end
143
143
144 def test_deleted_files_should_not_be_listed
144 def test_deleted_files_should_not_be_listed
145 assert_equal 0, @repository.changesets.count
145 assert_equal 0, @repository.changesets.count
146 @repository.fetch_changesets
146 @repository.fetch_changesets
147 @project.reload
147 @project.reload
148 assert_equal CHANGESETS_NUM, @repository.changesets.count
148 assert_equal CHANGESETS_NUM, @repository.changesets.count
149
149
150 entries = @repository.entries('sources')
150 entries = @repository.entries('sources')
151 assert entries.detect {|e| e.name == 'watchers_controller.rb'}
151 assert entries.detect {|e| e.name == 'watchers_controller.rb'}
152 assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
152 assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
153 end
153 end
154
154
155 def test_entries_rev3
155 def test_entries_rev3
156 assert_equal 0, @repository.changesets.count
156 assert_equal 0, @repository.changesets.count
157 @repository.fetch_changesets
157 @repository.fetch_changesets
158 @project.reload
158 @project.reload
159 assert_equal CHANGESETS_NUM, @repository.changesets.count
159 assert_equal CHANGESETS_NUM, @repository.changesets.count
160 entries = @repository.entries('', '3')
160 entries = @repository.entries('', '3')
161 assert_kind_of Redmine::Scm::Adapters::Entries, entries
161 assert_kind_of Redmine::Scm::Adapters::Entries, entries
162 assert_equal 3, entries.size
162 assert_equal 3, entries.size
163 assert_equal entries[2].name, "README"
163 assert_equal entries[2].name, "README"
164 assert_equal entries[2].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22)
164 assert_equal entries[2].lastrev.time, Time.gm(2007, 12, 13, 16, 27, 22)
165 assert_equal entries[2].lastrev.identifier, '3'
165 assert_equal entries[2].lastrev.identifier, '3'
166 assert_equal entries[2].lastrev.revision, '3'
166 assert_equal entries[2].lastrev.revision, '3'
167 assert_equal entries[2].lastrev.author, 'LANG'
167 assert_equal entries[2].lastrev.author, 'LANG'
168 end
168 end
169
169
170 def test_entries_invalid_path
170 def test_entries_invalid_path
171 assert_equal 0, @repository.changesets.count
171 assert_equal 0, @repository.changesets.count
172 @repository.fetch_changesets
172 @repository.fetch_changesets
173 @project.reload
173 @project.reload
174 assert_equal CHANGESETS_NUM, @repository.changesets.count
174 assert_equal CHANGESETS_NUM, @repository.changesets.count
175 assert_nil @repository.entries('missing')
175 assert_nil @repository.entries('missing')
176 assert_nil @repository.entries('missing', '3')
176 assert_nil @repository.entries('missing', '3')
177 end
177 end
178
178
179 def test_entries_invalid_revision
179 def test_entries_invalid_revision
180 assert_equal 0, @repository.changesets.count
180 assert_equal 0, @repository.changesets.count
181 @repository.fetch_changesets
181 @repository.fetch_changesets
182 @project.reload
182 @project.reload
183 assert_equal CHANGESETS_NUM, @repository.changesets.count
183 assert_equal CHANGESETS_NUM, @repository.changesets.count
184 assert_nil @repository.entries('', '123')
184 assert_nil @repository.entries('', '123')
185 end
185 end
186
186
187 def test_cat
187 def test_cat
188 assert_equal 0, @repository.changesets.count
188 assert_equal 0, @repository.changesets.count
189 @repository.fetch_changesets
189 @repository.fetch_changesets
190 @project.reload
190 @project.reload
191 assert_equal CHANGESETS_NUM, @repository.changesets.count
191 assert_equal CHANGESETS_NUM, @repository.changesets.count
192 buf = @repository.cat('README')
192 buf = @repository.cat('README')
193 assert buf
193 assert buf
194 lines = buf.split("\n")
194 lines = buf.split("\n")
195 assert_equal 3, lines.length
195 assert_equal 3, lines.length
196 buf = lines[1].gsub(/\r$/, "")
196 buf = lines[1].gsub(/\r$/, "")
197 assert_equal 'with one change', buf
197 assert_equal 'with one change', buf
198 buf = @repository.cat('README', '1')
198 buf = @repository.cat('README', '1')
199 assert buf
199 assert buf
200 lines = buf.split("\n")
200 lines = buf.split("\n")
201 assert_equal 1, lines.length
201 assert_equal 1, lines.length
202 buf = lines[0].gsub(/\r$/, "")
202 buf = lines[0].gsub(/\r$/, "")
203 assert_equal 'CVS test repository', buf
203 assert_equal 'CVS test repository', buf
204 assert_nil @repository.cat('missing.rb')
204 assert_nil @repository.cat('missing.rb')
205
205
206 # sources/welcome_controller.rb is removed at revision 5.
206 # sources/welcome_controller.rb is removed at revision 5.
207 assert @repository.cat('sources/welcome_controller.rb', '4')
207 assert @repository.cat('sources/welcome_controller.rb', '4')
208 assert @repository.cat('sources/welcome_controller.rb', '5').blank?
208 assert @repository.cat('sources/welcome_controller.rb', '5').blank?
209
209
210 # invalid revision
210 # invalid revision
211 assert @repository.cat('README', '123').blank?
211 assert @repository.cat('README', '123').blank?
212 end
212 end
213
213
214 def test_annotate
214 def test_annotate
215 assert_equal 0, @repository.changesets.count
215 assert_equal 0, @repository.changesets.count
216 @repository.fetch_changesets
216 @repository.fetch_changesets
217 @project.reload
217 @project.reload
218 assert_equal CHANGESETS_NUM, @repository.changesets.count
218 assert_equal CHANGESETS_NUM, @repository.changesets.count
219 ann = @repository.annotate('README')
219 ann = @repository.annotate('README')
220 assert ann
220 assert ann
221 assert_equal 3, ann.revisions.length
221 assert_equal 3, ann.revisions.length
222 assert_equal '1.2', ann.revisions[1].revision
222 assert_equal '1.2', ann.revisions[1].revision
223 assert_equal 'LANG', ann.revisions[1].author
223 assert_equal 'LANG', ann.revisions[1].author
224 assert_equal 'with one change', ann.lines[1]
224 assert_equal 'with one change', ann.lines[1]
225
225
226 ann = @repository.annotate('README', '1')
226 ann = @repository.annotate('README', '1')
227 assert ann
227 assert ann
228 assert_equal 1, ann.revisions.length
228 assert_equal 1, ann.revisions.length
229 assert_equal '1.1', ann.revisions[0].revision
229 assert_equal '1.1', ann.revisions[0].revision
230 assert_equal 'LANG', ann.revisions[0].author
230 assert_equal 'LANG', ann.revisions[0].author
231 assert_equal 'CVS test repository', ann.lines[0]
231 assert_equal 'CVS test repository', ann.lines[0]
232
232
233 # invalid revision
233 # invalid revision
234 assert_nil @repository.annotate('README', '123')
234 assert_nil @repository.annotate('README', '123')
235 end
235 end
236
236
237 else
237 else
238 puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
238 puts "CVS test repository NOT FOUND. Skipping unit tests !!!"
239 def test_fake; assert true end
239 def test_fake; assert true end
240 end
240 end
241 end
241 end
@@ -1,129 +1,129
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositoryDarcsTest < ActiveSupport::TestCase
20 class RepositoryDarcsTest < ActiveSupport::TestCase
21 fixtures :projects
21 fixtures :projects
22
22
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s
25 REPOSITORY_PATH = Rails.root.join('tmp/test/darcs_repository').to_s
26 NUM_REV = 6
26 NUM_REV = 6
27
27
28 def setup
28 def setup
29 @project = Project.find(3)
29 @project = Project.find(3)
30 @repository = Repository::Darcs.create(
30 @repository = Repository::Darcs.create(
31 :project => @project,
31 :project => @project,
32 :url => REPOSITORY_PATH,
32 :url => REPOSITORY_PATH,
33 :log_encoding => 'UTF-8'
33 :log_encoding => 'UTF-8'
34 )
34 )
35 assert @repository
35 assert @repository
36 end
36 end
37
37
38 def test_blank_path_to_repository_error_message
38 def test_blank_path_to_repository_error_message
39 set_language_if_valid 'en'
39 set_language_if_valid 'en'
40 repo = Repository::Darcs.new(
40 repo = Repository::Darcs.new(
41 :project => @project,
41 :project => @project,
42 :identifier => 'test',
42 :identifier => 'test',
43 :log_encoding => 'UTF-8'
43 :log_encoding => 'UTF-8'
44 )
44 )
45 assert !repo.save
45 assert !repo.save
46 assert_include "Path to repository can't be blank",
46 assert_include "Path to repository can't be blank",
47 repo.errors.full_messages
47 repo.errors.full_messages
48 end
48 end
49
49
50 def test_blank_path_to_repository_error_message_fr
50 def test_blank_path_to_repository_error_message_fr
51 set_language_if_valid 'fr'
51 set_language_if_valid 'fr'
52 str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)"
52 str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)"
53 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
53 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
54 repo = Repository::Darcs.new(
54 repo = Repository::Darcs.new(
55 :project => @project,
55 :project => @project,
56 :url => "",
56 :url => "",
57 :identifier => 'test',
57 :identifier => 'test',
58 :log_encoding => 'UTF-8'
58 :log_encoding => 'UTF-8'
59 )
59 )
60 assert !repo.save
60 assert !repo.save
61 assert_include str, repo.errors.full_messages
61 assert_include str, repo.errors.full_messages
62 end
62 end
63
63
64 if File.directory?(REPOSITORY_PATH)
64 if File.directory?(REPOSITORY_PATH)
65 def test_fetch_changesets_from_scratch
65 def test_fetch_changesets_from_scratch
66 assert_equal 0, @repository.changesets.count
66 assert_equal 0, @repository.changesets.count
67 @repository.fetch_changesets
67 @repository.fetch_changesets
68 @project.reload
68 @project.reload
69
69
70 assert_equal NUM_REV, @repository.changesets.count
70 assert_equal NUM_REV, @repository.changesets.count
71 assert_equal 13, @repository.filechanges.count
71 assert_equal 13, @repository.filechanges.count
72 assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments
72 assert_equal "Initial commit.", @repository.changesets.find_by_revision('1').comments
73 end
73 end
74
74
75 def test_fetch_changesets_incremental
75 def test_fetch_changesets_incremental
76 assert_equal 0, @repository.changesets.count
76 assert_equal 0, @repository.changesets.count
77 @repository.fetch_changesets
77 @repository.fetch_changesets
78 @project.reload
78 @project.reload
79 assert_equal NUM_REV, @repository.changesets.count
79 assert_equal NUM_REV, @repository.changesets.count
80
80
81 # Remove changesets with revision > 3
81 # Remove changesets with revision > 3
82 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 3}
82 @repository.changesets.all.each {|c| c.destroy if c.revision.to_i > 3}
83 @project.reload
83 @project.reload
84 assert_equal 3, @repository.changesets.count
84 assert_equal 3, @repository.changesets.count
85
85
86 @repository.fetch_changesets
86 @repository.fetch_changesets
87 @project.reload
87 @project.reload
88 assert_equal NUM_REV, @repository.changesets.count
88 assert_equal NUM_REV, @repository.changesets.count
89 end
89 end
90
90
91 def test_entries
91 def test_entries
92 entries = @repository.entries
92 entries = @repository.entries
93 assert_kind_of Redmine::Scm::Adapters::Entries, entries
93 assert_kind_of Redmine::Scm::Adapters::Entries, entries
94 end
94 end
95
95
96 def test_entries_invalid_revision
96 def test_entries_invalid_revision
97 assert_equal 0, @repository.changesets.count
97 assert_equal 0, @repository.changesets.count
98 @repository.fetch_changesets
98 @repository.fetch_changesets
99 @project.reload
99 @project.reload
100 assert_equal NUM_REV, @repository.changesets.count
100 assert_equal NUM_REV, @repository.changesets.count
101 assert_nil @repository.entries('', '123')
101 assert_nil @repository.entries('', '123')
102 end
102 end
103
103
104 def test_deleted_files_should_not_be_listed
104 def test_deleted_files_should_not_be_listed
105 assert_equal 0, @repository.changesets.count
105 assert_equal 0, @repository.changesets.count
106 @repository.fetch_changesets
106 @repository.fetch_changesets
107 @project.reload
107 @project.reload
108 assert_equal NUM_REV, @repository.changesets.count
108 assert_equal NUM_REV, @repository.changesets.count
109 entries = @repository.entries('sources')
109 entries = @repository.entries('sources')
110 assert entries.detect {|e| e.name == 'watchers_controller.rb'}
110 assert entries.detect {|e| e.name == 'watchers_controller.rb'}
111 assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
111 assert_nil entries.detect {|e| e.name == 'welcome_controller.rb'}
112 end
112 end
113
113
114 def test_cat
114 def test_cat
115 if @repository.scm.supports_cat?
115 if @repository.scm.supports_cat?
116 assert_equal 0, @repository.changesets.count
116 assert_equal 0, @repository.changesets.count
117 @repository.fetch_changesets
117 @repository.fetch_changesets
118 @project.reload
118 @project.reload
119 assert_equal NUM_REV, @repository.changesets.count
119 assert_equal NUM_REV, @repository.changesets.count
120 cat = @repository.cat("sources/welcome_controller.rb", 2)
120 cat = @repository.cat("sources/welcome_controller.rb", 2)
121 assert_not_nil cat
121 assert_not_nil cat
122 assert cat.include?('class WelcomeController < ApplicationController')
122 assert cat.include?('class WelcomeController < ApplicationController')
123 end
123 end
124 end
124 end
125 else
125 else
126 puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
126 puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
127 def test_fake; assert true end
127 def test_fake; assert true end
128 end
128 end
129 end
129 end
@@ -1,377 +1,377
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositoryMercurialTest < ActiveSupport::TestCase
20 class RepositoryMercurialTest < ActiveSupport::TestCase
21 fixtures :projects
21 fixtures :projects
22
22
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s
25 REPOSITORY_PATH = Rails.root.join('tmp/test/mercurial_repository').to_s
26 NUM_REV = 32
26 NUM_REV = 32
27 CHAR_1_HEX = "\xc3\x9c"
27 CHAR_1_HEX = "\xc3\x9c"
28
28
29 def setup
29 def setup
30 @project = Project.find(3)
30 @project = Project.find(3)
31 @repository = Repository::Mercurial.create(
31 @repository = Repository::Mercurial.create(
32 :project => @project,
32 :project => @project,
33 :url => REPOSITORY_PATH,
33 :url => REPOSITORY_PATH,
34 :path_encoding => 'ISO-8859-1'
34 :path_encoding => 'ISO-8859-1'
35 )
35 )
36 assert @repository
36 assert @repository
37 @char_1 = CHAR_1_HEX.dup
37 @char_1 = CHAR_1_HEX.dup
38 @tag_char_1 = "tag-#{CHAR_1_HEX}-00"
38 @tag_char_1 = "tag-#{CHAR_1_HEX}-00"
39 @branch_char_0 = "branch-#{CHAR_1_HEX}-00"
39 @branch_char_0 = "branch-#{CHAR_1_HEX}-00"
40 @branch_char_1 = "branch-#{CHAR_1_HEX}-01"
40 @branch_char_1 = "branch-#{CHAR_1_HEX}-01"
41 if @char_1.respond_to?(:force_encoding)
41 if @char_1.respond_to?(:force_encoding)
42 @char_1.force_encoding('UTF-8')
42 @char_1.force_encoding('UTF-8')
43 @tag_char_1.force_encoding('UTF-8')
43 @tag_char_1.force_encoding('UTF-8')
44 @branch_char_0.force_encoding('UTF-8')
44 @branch_char_0.force_encoding('UTF-8')
45 @branch_char_1.force_encoding('UTF-8')
45 @branch_char_1.force_encoding('UTF-8')
46 end
46 end
47 end
47 end
48
48
49
49
50 def test_blank_path_to_repository_error_message
50 def test_blank_path_to_repository_error_message
51 set_language_if_valid 'en'
51 set_language_if_valid 'en'
52 repo = Repository::Mercurial.new(
52 repo = Repository::Mercurial.new(
53 :project => @project,
53 :project => @project,
54 :identifier => 'test'
54 :identifier => 'test'
55 )
55 )
56 assert !repo.save
56 assert !repo.save
57 assert_include "Path to repository can't be blank",
57 assert_include "Path to repository can't be blank",
58 repo.errors.full_messages
58 repo.errors.full_messages
59 end
59 end
60
60
61 def test_blank_path_to_repository_error_message_fr
61 def test_blank_path_to_repository_error_message_fr
62 set_language_if_valid 'fr'
62 set_language_if_valid 'fr'
63 str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)"
63 str = "Chemin du d\xc3\xa9p\xc3\xb4t doit \xc3\xaatre renseign\xc3\xa9(e)"
64 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
64 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
65 repo = Repository::Mercurial.new(
65 repo = Repository::Mercurial.new(
66 :project => @project,
66 :project => @project,
67 :url => "",
67 :url => "",
68 :identifier => 'test',
68 :identifier => 'test',
69 :path_encoding => ''
69 :path_encoding => ''
70 )
70 )
71 assert !repo.save
71 assert !repo.save
72 assert_include str, repo.errors.full_messages
72 assert_include str, repo.errors.full_messages
73 end
73 end
74
74
75 if File.directory?(REPOSITORY_PATH)
75 if File.directory?(REPOSITORY_PATH)
76 def test_scm_available
76 def test_scm_available
77 klass = Repository::Mercurial
77 klass = Repository::Mercurial
78 assert_equal "Mercurial", klass.scm_name
78 assert_equal "Mercurial", klass.scm_name
79 assert klass.scm_adapter_class
79 assert klass.scm_adapter_class
80 assert_not_equal "", klass.scm_command
80 assert_not_equal "", klass.scm_command
81 assert_equal true, klass.scm_available
81 assert_equal true, klass.scm_available
82 end
82 end
83
83
84 def test_entries
84 def test_entries
85 entries = @repository.entries
85 entries = @repository.entries
86 assert_kind_of Redmine::Scm::Adapters::Entries, entries
86 assert_kind_of Redmine::Scm::Adapters::Entries, entries
87 end
87 end
88
88
89 def test_fetch_changesets_from_scratch
89 def test_fetch_changesets_from_scratch
90 assert_equal 0, @repository.changesets.count
90 assert_equal 0, @repository.changesets.count
91 @repository.fetch_changesets
91 @repository.fetch_changesets
92 @project.reload
92 @project.reload
93 assert_equal NUM_REV, @repository.changesets.count
93 assert_equal NUM_REV, @repository.changesets.count
94 assert_equal 46, @repository.filechanges.count
94 assert_equal 46, @repository.filechanges.count
95 assert_equal "Initial import.\nThe repository contains 3 files.",
95 assert_equal "Initial import.\nThe repository contains 3 files.",
96 @repository.changesets.find_by_revision('0').comments
96 @repository.changesets.find_by_revision('0').comments
97 end
97 end
98
98
99 def test_fetch_changesets_incremental
99 def test_fetch_changesets_incremental
100 assert_equal 0, @repository.changesets.count
100 assert_equal 0, @repository.changesets.count
101 @repository.fetch_changesets
101 @repository.fetch_changesets
102 @project.reload
102 @project.reload
103 assert_equal NUM_REV, @repository.changesets.count
103 assert_equal NUM_REV, @repository.changesets.count
104 # Remove changesets with revision > 2
104 # Remove changesets with revision > 2
105 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2}
105 @repository.changesets.all.each {|c| c.destroy if c.revision.to_i > 2}
106 @project.reload
106 @project.reload
107 assert_equal 3, @repository.changesets.count
107 assert_equal 3, @repository.changesets.count
108
108
109 @repository.fetch_changesets
109 @repository.fetch_changesets
110 @project.reload
110 @project.reload
111 assert_equal NUM_REV, @repository.changesets.count
111 assert_equal NUM_REV, @repository.changesets.count
112 end
112 end
113
113
114 def test_isodatesec
114 def test_isodatesec
115 # Template keyword 'isodatesec' supported in Mercurial 1.0 and higher
115 # Template keyword 'isodatesec' supported in Mercurial 1.0 and higher
116 if @repository.scm.class.client_version_above?([1, 0])
116 if @repository.scm.class.client_version_above?([1, 0])
117 assert_equal 0, @repository.changesets.count
117 assert_equal 0, @repository.changesets.count
118 @repository.fetch_changesets
118 @repository.fetch_changesets
119 @project.reload
119 @project.reload
120 assert_equal NUM_REV, @repository.changesets.count
120 assert_equal NUM_REV, @repository.changesets.count
121 rev0_committed_on = Time.gm(2007, 12, 14, 9, 22, 52)
121 rev0_committed_on = Time.gm(2007, 12, 14, 9, 22, 52)
122 assert_equal @repository.changesets.find_by_revision('0').committed_on, rev0_committed_on
122 assert_equal @repository.changesets.find_by_revision('0').committed_on, rev0_committed_on
123 end
123 end
124 end
124 end
125
125
126 def test_changeset_order_by_revision
126 def test_changeset_order_by_revision
127 assert_equal 0, @repository.changesets.count
127 assert_equal 0, @repository.changesets.count
128 @repository.fetch_changesets
128 @repository.fetch_changesets
129 @project.reload
129 @project.reload
130 assert_equal NUM_REV, @repository.changesets.count
130 assert_equal NUM_REV, @repository.changesets.count
131
131
132 c0 = @repository.latest_changeset
132 c0 = @repository.latest_changeset
133 c1 = @repository.changesets.find_by_revision('0')
133 c1 = @repository.changesets.find_by_revision('0')
134 # sorted by revision (id), not by date
134 # sorted by revision (id), not by date
135 assert c0.revision.to_i > c1.revision.to_i
135 assert c0.revision.to_i > c1.revision.to_i
136 assert c0.committed_on < c1.committed_on
136 assert c0.committed_on < c1.committed_on
137 end
137 end
138
138
139 def test_latest_changesets
139 def test_latest_changesets
140 assert_equal 0, @repository.changesets.count
140 assert_equal 0, @repository.changesets.count
141 @repository.fetch_changesets
141 @repository.fetch_changesets
142 @project.reload
142 @project.reload
143 assert_equal NUM_REV, @repository.changesets.count
143 assert_equal NUM_REV, @repository.changesets.count
144
144
145 # with_limit
145 # with_limit
146 changesets = @repository.latest_changesets('', nil, 2)
146 changesets = @repository.latest_changesets('', nil, 2)
147 assert_equal %w|31 30|, changesets.collect(&:revision)
147 assert_equal %w|31 30|, changesets.collect(&:revision)
148
148
149 # with_filepath
149 # with_filepath
150 changesets = @repository.latest_changesets(
150 changesets = @repository.latest_changesets(
151 '/sql_escape/percent%dir/percent%file1.txt', nil)
151 '/sql_escape/percent%dir/percent%file1.txt', nil)
152 assert_equal %w|30 11 10 9|, changesets.collect(&:revision)
152 assert_equal %w|30 11 10 9|, changesets.collect(&:revision)
153
153
154 changesets = @repository.latest_changesets(
154 changesets = @repository.latest_changesets(
155 '/sql_escape/underscore_dir/understrike_file.txt', nil)
155 '/sql_escape/underscore_dir/understrike_file.txt', nil)
156 assert_equal %w|30 12 9|, changesets.collect(&:revision)
156 assert_equal %w|30 12 9|, changesets.collect(&:revision)
157
157
158 changesets = @repository.latest_changesets('README', nil)
158 changesets = @repository.latest_changesets('README', nil)
159 assert_equal %w|31 30 28 17 8 6 1 0|, changesets.collect(&:revision)
159 assert_equal %w|31 30 28 17 8 6 1 0|, changesets.collect(&:revision)
160
160
161 changesets = @repository.latest_changesets('README','8')
161 changesets = @repository.latest_changesets('README','8')
162 assert_equal %w|8 6 1 0|, changesets.collect(&:revision)
162 assert_equal %w|8 6 1 0|, changesets.collect(&:revision)
163
163
164 changesets = @repository.latest_changesets('README','8', 2)
164 changesets = @repository.latest_changesets('README','8', 2)
165 assert_equal %w|8 6|, changesets.collect(&:revision)
165 assert_equal %w|8 6|, changesets.collect(&:revision)
166
166
167 # with_dirpath
167 # with_dirpath
168 changesets = @repository.latest_changesets('images', nil)
168 changesets = @repository.latest_changesets('images', nil)
169 assert_equal %w|1 0|, changesets.collect(&:revision)
169 assert_equal %w|1 0|, changesets.collect(&:revision)
170
170
171 path = 'sql_escape/percent%dir'
171 path = 'sql_escape/percent%dir'
172 changesets = @repository.latest_changesets(path, nil)
172 changesets = @repository.latest_changesets(path, nil)
173 assert_equal %w|30 13 11 10 9|, changesets.collect(&:revision)
173 assert_equal %w|30 13 11 10 9|, changesets.collect(&:revision)
174
174
175 changesets = @repository.latest_changesets(path, '11')
175 changesets = @repository.latest_changesets(path, '11')
176 assert_equal %w|11 10 9|, changesets.collect(&:revision)
176 assert_equal %w|11 10 9|, changesets.collect(&:revision)
177
177
178 changesets = @repository.latest_changesets(path, '11', 2)
178 changesets = @repository.latest_changesets(path, '11', 2)
179 assert_equal %w|11 10|, changesets.collect(&:revision)
179 assert_equal %w|11 10|, changesets.collect(&:revision)
180
180
181 path = 'sql_escape/underscore_dir'
181 path = 'sql_escape/underscore_dir'
182 changesets = @repository.latest_changesets(path, nil)
182 changesets = @repository.latest_changesets(path, nil)
183 assert_equal %w|30 13 12 9|, changesets.collect(&:revision)
183 assert_equal %w|30 13 12 9|, changesets.collect(&:revision)
184
184
185 changesets = @repository.latest_changesets(path, '12')
185 changesets = @repository.latest_changesets(path, '12')
186 assert_equal %w|12 9|, changesets.collect(&:revision)
186 assert_equal %w|12 9|, changesets.collect(&:revision)
187
187
188 changesets = @repository.latest_changesets(path, '12', 1)
188 changesets = @repository.latest_changesets(path, '12', 1)
189 assert_equal %w|12|, changesets.collect(&:revision)
189 assert_equal %w|12|, changesets.collect(&:revision)
190
190
191 # tag
191 # tag
192 changesets = @repository.latest_changesets('', 'tag_test.00')
192 changesets = @repository.latest_changesets('', 'tag_test.00')
193 assert_equal %w|5 4 3 2 1 0|, changesets.collect(&:revision)
193 assert_equal %w|5 4 3 2 1 0|, changesets.collect(&:revision)
194
194
195 changesets = @repository.latest_changesets('', 'tag_test.00', 2)
195 changesets = @repository.latest_changesets('', 'tag_test.00', 2)
196 assert_equal %w|5 4|, changesets.collect(&:revision)
196 assert_equal %w|5 4|, changesets.collect(&:revision)
197
197
198 changesets = @repository.latest_changesets('sources', 'tag_test.00')
198 changesets = @repository.latest_changesets('sources', 'tag_test.00')
199 assert_equal %w|4 3 2 1 0|, changesets.collect(&:revision)
199 assert_equal %w|4 3 2 1 0|, changesets.collect(&:revision)
200
200
201 changesets = @repository.latest_changesets('sources', 'tag_test.00', 2)
201 changesets = @repository.latest_changesets('sources', 'tag_test.00', 2)
202 assert_equal %w|4 3|, changesets.collect(&:revision)
202 assert_equal %w|4 3|, changesets.collect(&:revision)
203
203
204 # named branch
204 # named branch
205 if @repository.scm.class.client_version_above?([1, 6])
205 if @repository.scm.class.client_version_above?([1, 6])
206 changesets = @repository.latest_changesets('', @branch_char_1)
206 changesets = @repository.latest_changesets('', @branch_char_1)
207 assert_equal %w|27 26|, changesets.collect(&:revision)
207 assert_equal %w|27 26|, changesets.collect(&:revision)
208 end
208 end
209
209
210 changesets = @repository.latest_changesets("latin-1-dir/test-#{@char_1}-subdir", @branch_char_1)
210 changesets = @repository.latest_changesets("latin-1-dir/test-#{@char_1}-subdir", @branch_char_1)
211 assert_equal %w|27|, changesets.collect(&:revision)
211 assert_equal %w|27|, changesets.collect(&:revision)
212 end
212 end
213
213
214 def test_copied_files
214 def test_copied_files
215 assert_equal 0, @repository.changesets.count
215 assert_equal 0, @repository.changesets.count
216 @repository.fetch_changesets
216 @repository.fetch_changesets
217 @project.reload
217 @project.reload
218 assert_equal NUM_REV, @repository.changesets.count
218 assert_equal NUM_REV, @repository.changesets.count
219
219
220 cs1 = @repository.changesets.find_by_revision('13')
220 cs1 = @repository.changesets.find_by_revision('13')
221 assert_not_nil cs1
221 assert_not_nil cs1
222 c1 = cs1.filechanges.sort_by(&:path)
222 c1 = cs1.filechanges.sort_by(&:path)
223 assert_equal 2, c1.size
223 assert_equal 2, c1.size
224
224
225 assert_equal 'A', c1[0].action
225 assert_equal 'A', c1[0].action
226 assert_equal '/sql_escape/percent%dir/percentfile1.txt', c1[0].path
226 assert_equal '/sql_escape/percent%dir/percentfile1.txt', c1[0].path
227 assert_equal '/sql_escape/percent%dir/percent%file1.txt', c1[0].from_path
227 assert_equal '/sql_escape/percent%dir/percent%file1.txt', c1[0].from_path
228 assert_equal '3a330eb32958', c1[0].from_revision
228 assert_equal '3a330eb32958', c1[0].from_revision
229
229
230 assert_equal 'A', c1[1].action
230 assert_equal 'A', c1[1].action
231 assert_equal '/sql_escape/underscore_dir/understrike-file.txt', c1[1].path
231 assert_equal '/sql_escape/underscore_dir/understrike-file.txt', c1[1].path
232 assert_equal '/sql_escape/underscore_dir/understrike_file.txt', c1[1].from_path
232 assert_equal '/sql_escape/underscore_dir/understrike_file.txt', c1[1].from_path
233
233
234 cs2 = @repository.changesets.find_by_revision('15')
234 cs2 = @repository.changesets.find_by_revision('15')
235 c2 = cs2.filechanges
235 c2 = cs2.filechanges
236 assert_equal 1, c2.size
236 assert_equal 1, c2.size
237
237
238 assert_equal 'A', c2[0].action
238 assert_equal 'A', c2[0].action
239 assert_equal '/README (1)[2]&,%.-3_4', c2[0].path
239 assert_equal '/README (1)[2]&,%.-3_4', c2[0].path
240 assert_equal '/README', c2[0].from_path
240 assert_equal '/README', c2[0].from_path
241 assert_equal '933ca60293d7', c2[0].from_revision
241 assert_equal '933ca60293d7', c2[0].from_revision
242
242
243 cs3 = @repository.changesets.find_by_revision('19')
243 cs3 = @repository.changesets.find_by_revision('19')
244 c3 = cs3.filechanges
244 c3 = cs3.filechanges
245 assert_equal 1, c3.size
245 assert_equal 1, c3.size
246 assert_equal 'A', c3[0].action
246 assert_equal 'A', c3[0].action
247 assert_equal "/latin-1-dir/test-#{@char_1}-1.txt", c3[0].path
247 assert_equal "/latin-1-dir/test-#{@char_1}-1.txt", c3[0].path
248 assert_equal "/latin-1-dir/test-#{@char_1}.txt", c3[0].from_path
248 assert_equal "/latin-1-dir/test-#{@char_1}.txt", c3[0].from_path
249 assert_equal '5d9891a1b425', c3[0].from_revision
249 assert_equal '5d9891a1b425', c3[0].from_revision
250 end
250 end
251
251
252 def test_find_changeset_by_name
252 def test_find_changeset_by_name
253 assert_equal 0, @repository.changesets.count
253 assert_equal 0, @repository.changesets.count
254 @repository.fetch_changesets
254 @repository.fetch_changesets
255 @project.reload
255 @project.reload
256 assert_equal NUM_REV, @repository.changesets.count
256 assert_equal NUM_REV, @repository.changesets.count
257 %w|2 400bb8672109 400|.each do |r|
257 %w|2 400bb8672109 400|.each do |r|
258 assert_equal '2', @repository.find_changeset_by_name(r).revision
258 assert_equal '2', @repository.find_changeset_by_name(r).revision
259 end
259 end
260 end
260 end
261
261
262 def test_find_changeset_by_invalid_name
262 def test_find_changeset_by_invalid_name
263 assert_equal 0, @repository.changesets.count
263 assert_equal 0, @repository.changesets.count
264 @repository.fetch_changesets
264 @repository.fetch_changesets
265 @project.reload
265 @project.reload
266 assert_equal NUM_REV, @repository.changesets.count
266 assert_equal NUM_REV, @repository.changesets.count
267 assert_nil @repository.find_changeset_by_name('100000')
267 assert_nil @repository.find_changeset_by_name('100000')
268 end
268 end
269
269
270 def test_identifier
270 def test_identifier
271 assert_equal 0, @repository.changesets.count
271 assert_equal 0, @repository.changesets.count
272 @repository.fetch_changesets
272 @repository.fetch_changesets
273 @project.reload
273 @project.reload
274 assert_equal NUM_REV, @repository.changesets.count
274 assert_equal NUM_REV, @repository.changesets.count
275 c = @repository.changesets.find_by_revision('2')
275 c = @repository.changesets.find_by_revision('2')
276 assert_equal c.scmid, c.identifier
276 assert_equal c.scmid, c.identifier
277 end
277 end
278
278
279 def test_format_identifier
279 def test_format_identifier
280 assert_equal 0, @repository.changesets.count
280 assert_equal 0, @repository.changesets.count
281 @repository.fetch_changesets
281 @repository.fetch_changesets
282 @project.reload
282 @project.reload
283 assert_equal NUM_REV, @repository.changesets.count
283 assert_equal NUM_REV, @repository.changesets.count
284 c = @repository.changesets.find_by_revision('2')
284 c = @repository.changesets.find_by_revision('2')
285 assert_equal '2:400bb8672109', c.format_identifier
285 assert_equal '2:400bb8672109', c.format_identifier
286 end
286 end
287
287
288 def test_find_changeset_by_empty_name
288 def test_find_changeset_by_empty_name
289 assert_equal 0, @repository.changesets.count
289 assert_equal 0, @repository.changesets.count
290 @repository.fetch_changesets
290 @repository.fetch_changesets
291 @project.reload
291 @project.reload
292 assert_equal NUM_REV, @repository.changesets.count
292 assert_equal NUM_REV, @repository.changesets.count
293 ['', ' ', nil].each do |r|
293 ['', ' ', nil].each do |r|
294 assert_nil @repository.find_changeset_by_name(r)
294 assert_nil @repository.find_changeset_by_name(r)
295 end
295 end
296 end
296 end
297
297
298 def test_parents
298 def test_parents
299 assert_equal 0, @repository.changesets.count
299 assert_equal 0, @repository.changesets.count
300 @repository.fetch_changesets
300 @repository.fetch_changesets
301 @project.reload
301 @project.reload
302 assert_equal NUM_REV, @repository.changesets.count
302 assert_equal NUM_REV, @repository.changesets.count
303 r1 = @repository.changesets.find_by_revision('0')
303 r1 = @repository.changesets.find_by_revision('0')
304 assert_equal [], r1.parents
304 assert_equal [], r1.parents
305 r2 = @repository.changesets.find_by_revision('1')
305 r2 = @repository.changesets.find_by_revision('1')
306 assert_equal 1, r2.parents.length
306 assert_equal 1, r2.parents.length
307 assert_equal "0885933ad4f6",
307 assert_equal "0885933ad4f6",
308 r2.parents[0].identifier
308 r2.parents[0].identifier
309 r3 = @repository.changesets.find_by_revision('30')
309 r3 = @repository.changesets.find_by_revision('30')
310 assert_equal 2, r3.parents.length
310 assert_equal 2, r3.parents.length
311 r4 = [r3.parents[0].identifier, r3.parents[1].identifier].sort
311 r4 = [r3.parents[0].identifier, r3.parents[1].identifier].sort
312 assert_equal "3a330eb32958", r4[0]
312 assert_equal "3a330eb32958", r4[0]
313 assert_equal "a94b0528f24f", r4[1]
313 assert_equal "a94b0528f24f", r4[1]
314 end
314 end
315
315
316 def test_activities
316 def test_activities
317 c = Changeset.new(:repository => @repository,
317 c = Changeset.new(:repository => @repository,
318 :committed_on => Time.now,
318 :committed_on => Time.now,
319 :revision => '123',
319 :revision => '123',
320 :scmid => 'abc400bb8672',
320 :scmid => 'abc400bb8672',
321 :comments => 'test')
321 :comments => 'test')
322 assert c.event_title.include?('123:abc400bb8672:')
322 assert c.event_title.include?('123:abc400bb8672:')
323 assert_equal 'abc400bb8672', c.event_url[:rev]
323 assert_equal 'abc400bb8672', c.event_url[:rev]
324 end
324 end
325
325
326 def test_previous
326 def test_previous
327 assert_equal 0, @repository.changesets.count
327 assert_equal 0, @repository.changesets.count
328 @repository.fetch_changesets
328 @repository.fetch_changesets
329 @project.reload
329 @project.reload
330 assert_equal NUM_REV, @repository.changesets.count
330 assert_equal NUM_REV, @repository.changesets.count
331 %w|28 3ae45e2d177d 3ae45|.each do |r1|
331 %w|28 3ae45e2d177d 3ae45|.each do |r1|
332 changeset = @repository.find_changeset_by_name(r1)
332 changeset = @repository.find_changeset_by_name(r1)
333 %w|27 7bbf4c738e71 7bbf|.each do |r2|
333 %w|27 7bbf4c738e71 7bbf|.each do |r2|
334 assert_equal @repository.find_changeset_by_name(r2), changeset.previous
334 assert_equal @repository.find_changeset_by_name(r2), changeset.previous
335 end
335 end
336 end
336 end
337 end
337 end
338
338
339 def test_previous_nil
339 def test_previous_nil
340 assert_equal 0, @repository.changesets.count
340 assert_equal 0, @repository.changesets.count
341 @repository.fetch_changesets
341 @repository.fetch_changesets
342 @project.reload
342 @project.reload
343 assert_equal NUM_REV, @repository.changesets.count
343 assert_equal NUM_REV, @repository.changesets.count
344 %w|0 0885933ad4f6 0885|.each do |r1|
344 %w|0 0885933ad4f6 0885|.each do |r1|
345 changeset = @repository.find_changeset_by_name(r1)
345 changeset = @repository.find_changeset_by_name(r1)
346 assert_nil changeset.previous
346 assert_nil changeset.previous
347 end
347 end
348 end
348 end
349
349
350 def test_next
350 def test_next
351 assert_equal 0, @repository.changesets.count
351 assert_equal 0, @repository.changesets.count
352 @repository.fetch_changesets
352 @repository.fetch_changesets
353 @project.reload
353 @project.reload
354 assert_equal NUM_REV, @repository.changesets.count
354 assert_equal NUM_REV, @repository.changesets.count
355 %w|27 7bbf4c738e71 7bbf|.each do |r2|
355 %w|27 7bbf4c738e71 7bbf|.each do |r2|
356 changeset = @repository.find_changeset_by_name(r2)
356 changeset = @repository.find_changeset_by_name(r2)
357 %w|28 3ae45e2d177d 3ae45|.each do |r1|
357 %w|28 3ae45e2d177d 3ae45|.each do |r1|
358 assert_equal @repository.find_changeset_by_name(r1), changeset.next
358 assert_equal @repository.find_changeset_by_name(r1), changeset.next
359 end
359 end
360 end
360 end
361 end
361 end
362
362
363 def test_next_nil
363 def test_next_nil
364 assert_equal 0, @repository.changesets.count
364 assert_equal 0, @repository.changesets.count
365 @repository.fetch_changesets
365 @repository.fetch_changesets
366 @project.reload
366 @project.reload
367 assert_equal NUM_REV, @repository.changesets.count
367 assert_equal NUM_REV, @repository.changesets.count
368 %w|31 31eeee7395c8 31eee|.each do |r1|
368 %w|31 31eeee7395c8 31eee|.each do |r1|
369 changeset = @repository.find_changeset_by_name(r1)
369 changeset = @repository.find_changeset_by_name(r1)
370 assert_nil changeset.next
370 assert_nil changeset.next
371 end
371 end
372 end
372 end
373 else
373 else
374 puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
374 puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
375 def test_fake; assert true end
375 def test_fake; assert true end
376 end
376 end
377 end
377 end
@@ -1,231 +1,231
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RepositorySubversionTest < ActiveSupport::TestCase
20 class RepositorySubversionTest < ActiveSupport::TestCase
21 fixtures :projects, :repositories, :enabled_modules, :users, :roles
21 fixtures :projects, :repositories, :enabled_modules, :users, :roles
22
22
23 NUM_REV = 11
23 NUM_REV = 11
24
24
25 def setup
25 def setup
26 @project = Project.find(3)
26 @project = Project.find(3)
27 @repository = Repository::Subversion.create(:project => @project,
27 @repository = Repository::Subversion.create(:project => @project,
28 :url => self.class.subversion_repository_url)
28 :url => self.class.subversion_repository_url)
29 assert @repository
29 assert @repository
30 end
30 end
31
31
32 if repository_configured?('subversion')
32 if repository_configured?('subversion')
33 def test_fetch_changesets_from_scratch
33 def test_fetch_changesets_from_scratch
34 assert_equal 0, @repository.changesets.count
34 assert_equal 0, @repository.changesets.count
35 @repository.fetch_changesets
35 @repository.fetch_changesets
36 @project.reload
36 @project.reload
37
37
38 assert_equal NUM_REV, @repository.changesets.count
38 assert_equal NUM_REV, @repository.changesets.count
39 assert_equal 20, @repository.filechanges.count
39 assert_equal 20, @repository.filechanges.count
40 assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
40 assert_equal 'Initial import.', @repository.changesets.find_by_revision('1').comments
41 end
41 end
42
42
43 def test_fetch_changesets_incremental
43 def test_fetch_changesets_incremental
44 assert_equal 0, @repository.changesets.count
44 assert_equal 0, @repository.changesets.count
45 @repository.fetch_changesets
45 @repository.fetch_changesets
46 @project.reload
46 @project.reload
47 assert_equal NUM_REV, @repository.changesets.count
47 assert_equal NUM_REV, @repository.changesets.count
48
48
49 # Remove changesets with revision > 5
49 # Remove changesets with revision > 5
50 @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 5}
50 @repository.changesets.all.each {|c| c.destroy if c.revision.to_i > 5}
51 @project.reload
51 @project.reload
52 assert_equal 5, @repository.changesets.count
52 assert_equal 5, @repository.changesets.count
53
53
54 @repository.fetch_changesets
54 @repository.fetch_changesets
55 @project.reload
55 @project.reload
56 assert_equal NUM_REV, @repository.changesets.count
56 assert_equal NUM_REV, @repository.changesets.count
57 end
57 end
58
58
59 def test_entries
59 def test_entries
60 entries = @repository.entries
60 entries = @repository.entries
61 assert_kind_of Redmine::Scm::Adapters::Entries, entries
61 assert_kind_of Redmine::Scm::Adapters::Entries, entries
62 end
62 end
63
63
64 def test_entries_for_invalid_path_should_return_nil
64 def test_entries_for_invalid_path_should_return_nil
65 entries = @repository.entries('invalid_path')
65 entries = @repository.entries('invalid_path')
66 assert_nil entries
66 assert_nil entries
67 end
67 end
68
68
69 def test_latest_changesets
69 def test_latest_changesets
70 assert_equal 0, @repository.changesets.count
70 assert_equal 0, @repository.changesets.count
71 @repository.fetch_changesets
71 @repository.fetch_changesets
72 @project.reload
72 @project.reload
73 assert_equal NUM_REV, @repository.changesets.count
73 assert_equal NUM_REV, @repository.changesets.count
74
74
75 # with limit
75 # with limit
76 changesets = @repository.latest_changesets('', nil, 2)
76 changesets = @repository.latest_changesets('', nil, 2)
77 assert_equal 2, changesets.size
77 assert_equal 2, changesets.size
78 assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets
78 assert_equal @repository.latest_changesets('', nil).slice(0,2), changesets
79
79
80 # with path
80 # with path
81 changesets = @repository.latest_changesets('subversion_test/folder', nil)
81 changesets = @repository.latest_changesets('subversion_test/folder', nil)
82 assert_equal ["10", "9", "7", "6", "5", "2"], changesets.collect(&:revision)
82 assert_equal ["10", "9", "7", "6", "5", "2"], changesets.collect(&:revision)
83
83
84 # with path and revision
84 # with path and revision
85 changesets = @repository.latest_changesets('subversion_test/folder', 8)
85 changesets = @repository.latest_changesets('subversion_test/folder', 8)
86 assert_equal ["7", "6", "5", "2"], changesets.collect(&:revision)
86 assert_equal ["7", "6", "5", "2"], changesets.collect(&:revision)
87 end
87 end
88
88
89 def test_directory_listing_with_square_brackets_in_path
89 def test_directory_listing_with_square_brackets_in_path
90 assert_equal 0, @repository.changesets.count
90 assert_equal 0, @repository.changesets.count
91 @repository.fetch_changesets
91 @repository.fetch_changesets
92 @project.reload
92 @project.reload
93 assert_equal NUM_REV, @repository.changesets.count
93 assert_equal NUM_REV, @repository.changesets.count
94
94
95 entries = @repository.entries('subversion_test/[folder_with_brackets]')
95 entries = @repository.entries('subversion_test/[folder_with_brackets]')
96 assert_not_nil entries, 'Expect to find entries in folder_with_brackets'
96 assert_not_nil entries, 'Expect to find entries in folder_with_brackets'
97 assert_equal 1, entries.size, 'Expect one entry in folder_with_brackets'
97 assert_equal 1, entries.size, 'Expect one entry in folder_with_brackets'
98 assert_equal 'README.txt', entries.first.name
98 assert_equal 'README.txt', entries.first.name
99 end
99 end
100
100
101 def test_directory_listing_with_square_brackets_in_base
101 def test_directory_listing_with_square_brackets_in_base
102 @project = Project.find(3)
102 @project = Project.find(3)
103 @repository = Repository::Subversion.create(
103 @repository = Repository::Subversion.create(
104 :project => @project,
104 :project => @project,
105 :url => "file:///#{self.class.repository_path('subversion')}/subversion_test/[folder_with_brackets]")
105 :url => "file:///#{self.class.repository_path('subversion')}/subversion_test/[folder_with_brackets]")
106
106
107 assert_equal 0, @repository.changesets.count
107 assert_equal 0, @repository.changesets.count
108 @repository.fetch_changesets
108 @repository.fetch_changesets
109 @project.reload
109 @project.reload
110
110
111 assert_equal 1, @repository.changesets.count, 'Expected to see 1 revision'
111 assert_equal 1, @repository.changesets.count, 'Expected to see 1 revision'
112 assert_equal 2, @repository.filechanges.count, 'Expected to see 2 changes, dir add and file add'
112 assert_equal 2, @repository.filechanges.count, 'Expected to see 2 changes, dir add and file add'
113
113
114 entries = @repository.entries('')
114 entries = @repository.entries('')
115 assert_not_nil entries, 'Expect to find entries'
115 assert_not_nil entries, 'Expect to find entries'
116 assert_equal 1, entries.size, 'Expect a single entry'
116 assert_equal 1, entries.size, 'Expect a single entry'
117 assert_equal 'README.txt', entries.first.name
117 assert_equal 'README.txt', entries.first.name
118 end
118 end
119
119
120 def test_identifier
120 def test_identifier
121 assert_equal 0, @repository.changesets.count
121 assert_equal 0, @repository.changesets.count
122 @repository.fetch_changesets
122 @repository.fetch_changesets
123 @project.reload
123 @project.reload
124 assert_equal NUM_REV, @repository.changesets.count
124 assert_equal NUM_REV, @repository.changesets.count
125 c = @repository.changesets.find_by_revision('1')
125 c = @repository.changesets.find_by_revision('1')
126 assert_equal c.revision, c.identifier
126 assert_equal c.revision, c.identifier
127 end
127 end
128
128
129 def test_find_changeset_by_empty_name
129 def test_find_changeset_by_empty_name
130 assert_equal 0, @repository.changesets.count
130 assert_equal 0, @repository.changesets.count
131 @repository.fetch_changesets
131 @repository.fetch_changesets
132 @project.reload
132 @project.reload
133 assert_equal NUM_REV, @repository.changesets.count
133 assert_equal NUM_REV, @repository.changesets.count
134 ['', ' ', nil].each do |r|
134 ['', ' ', nil].each do |r|
135 assert_nil @repository.find_changeset_by_name(r)
135 assert_nil @repository.find_changeset_by_name(r)
136 end
136 end
137 end
137 end
138
138
139 def test_identifier_nine_digit
139 def test_identifier_nine_digit
140 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
140 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
141 :revision => '123456789', :comments => 'test')
141 :revision => '123456789', :comments => 'test')
142 assert_equal c.identifier, c.revision
142 assert_equal c.identifier, c.revision
143 end
143 end
144
144
145 def test_format_identifier
145 def test_format_identifier
146 assert_equal 0, @repository.changesets.count
146 assert_equal 0, @repository.changesets.count
147 @repository.fetch_changesets
147 @repository.fetch_changesets
148 @project.reload
148 @project.reload
149 assert_equal NUM_REV, @repository.changesets.count
149 assert_equal NUM_REV, @repository.changesets.count
150 c = @repository.changesets.find_by_revision('1')
150 c = @repository.changesets.find_by_revision('1')
151 assert_equal c.format_identifier, c.revision
151 assert_equal c.format_identifier, c.revision
152 end
152 end
153
153
154 def test_format_identifier_nine_digit
154 def test_format_identifier_nine_digit
155 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
155 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
156 :revision => '123456789', :comments => 'test')
156 :revision => '123456789', :comments => 'test')
157 assert_equal c.format_identifier, c.revision
157 assert_equal c.format_identifier, c.revision
158 end
158 end
159
159
160 def test_activities
160 def test_activities
161 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
161 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
162 :revision => '1', :comments => 'test')
162 :revision => '1', :comments => 'test')
163 assert c.event_title.include?('1:')
163 assert c.event_title.include?('1:')
164 assert_equal '1', c.event_url[:rev]
164 assert_equal '1', c.event_url[:rev]
165 end
165 end
166
166
167 def test_activities_nine_digit
167 def test_activities_nine_digit
168 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
168 c = Changeset.new(:repository => @repository, :committed_on => Time.now,
169 :revision => '123456789', :comments => 'test')
169 :revision => '123456789', :comments => 'test')
170 assert c.event_title.include?('123456789:')
170 assert c.event_title.include?('123456789:')
171 assert_equal '123456789', c.event_url[:rev]
171 assert_equal '123456789', c.event_url[:rev]
172 end
172 end
173
173
174 def test_log_encoding_ignore_setting
174 def test_log_encoding_ignore_setting
175 with_settings :commit_logs_encoding => 'windows-1252' do
175 with_settings :commit_logs_encoding => 'windows-1252' do
176 s1 = "\xC2\x80"
176 s1 = "\xC2\x80"
177 s2 = "\xc3\x82\xc2\x80"
177 s2 = "\xc3\x82\xc2\x80"
178 if s1.respond_to?(:force_encoding)
178 if s1.respond_to?(:force_encoding)
179 s1.force_encoding('ISO-8859-1')
179 s1.force_encoding('ISO-8859-1')
180 s2.force_encoding('UTF-8')
180 s2.force_encoding('UTF-8')
181 assert_equal s1.encode('UTF-8'), s2
181 assert_equal s1.encode('UTF-8'), s2
182 end
182 end
183 c = Changeset.new(:repository => @repository,
183 c = Changeset.new(:repository => @repository,
184 :comments => s2,
184 :comments => s2,
185 :revision => '123',
185 :revision => '123',
186 :committed_on => Time.now)
186 :committed_on => Time.now)
187 assert c.save
187 assert c.save
188 assert_equal s2, c.comments
188 assert_equal s2, c.comments
189 end
189 end
190 end
190 end
191
191
192 def test_previous
192 def test_previous
193 assert_equal 0, @repository.changesets.count
193 assert_equal 0, @repository.changesets.count
194 @repository.fetch_changesets
194 @repository.fetch_changesets
195 @project.reload
195 @project.reload
196 assert_equal NUM_REV, @repository.changesets.count
196 assert_equal NUM_REV, @repository.changesets.count
197 changeset = @repository.find_changeset_by_name('3')
197 changeset = @repository.find_changeset_by_name('3')
198 assert_equal @repository.find_changeset_by_name('2'), changeset.previous
198 assert_equal @repository.find_changeset_by_name('2'), changeset.previous
199 end
199 end
200
200
201 def test_previous_nil
201 def test_previous_nil
202 assert_equal 0, @repository.changesets.count
202 assert_equal 0, @repository.changesets.count
203 @repository.fetch_changesets
203 @repository.fetch_changesets
204 @project.reload
204 @project.reload
205 assert_equal NUM_REV, @repository.changesets.count
205 assert_equal NUM_REV, @repository.changesets.count
206 changeset = @repository.find_changeset_by_name('1')
206 changeset = @repository.find_changeset_by_name('1')
207 assert_nil changeset.previous
207 assert_nil changeset.previous
208 end
208 end
209
209
210 def test_next
210 def test_next
211 assert_equal 0, @repository.changesets.count
211 assert_equal 0, @repository.changesets.count
212 @repository.fetch_changesets
212 @repository.fetch_changesets
213 @project.reload
213 @project.reload
214 assert_equal NUM_REV, @repository.changesets.count
214 assert_equal NUM_REV, @repository.changesets.count
215 changeset = @repository.find_changeset_by_name('2')
215 changeset = @repository.find_changeset_by_name('2')
216 assert_equal @repository.find_changeset_by_name('3'), changeset.next
216 assert_equal @repository.find_changeset_by_name('3'), changeset.next
217 end
217 end
218
218
219 def test_next_nil
219 def test_next_nil
220 assert_equal 0, @repository.changesets.count
220 assert_equal 0, @repository.changesets.count
221 @repository.fetch_changesets
221 @repository.fetch_changesets
222 @project.reload
222 @project.reload
223 assert_equal NUM_REV, @repository.changesets.count
223 assert_equal NUM_REV, @repository.changesets.count
224 changeset = @repository.find_changeset_by_name('11')
224 changeset = @repository.find_changeset_by_name('11')
225 assert_nil changeset.next
225 assert_nil changeset.next
226 end
226 end
227 else
227 else
228 puts "Subversion test repository NOT FOUND. Skipping unit tests !!!"
228 puts "Subversion test repository NOT FOUND. Skipping unit tests !!!"
229 def test_fake; assert true end
229 def test_fake; assert true end
230 end
230 end
231 end
231 end
General Comments 0
You need to be logged in to leave comments. Login now