##// END OF EJS Templates
Merged r14215 (#19482)....
Jean-Philippe Lang -
r13834:1c8527b8c166
parent child
Show More
@@ -1,714 +1,718
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 FieldFormat
19 module FieldFormat
20 def self.add(name, klass)
20 def self.add(name, klass)
21 all[name.to_s] = klass.instance
21 all[name.to_s] = klass.instance
22 end
22 end
23
23
24 def self.delete(name)
24 def self.delete(name)
25 all.delete(name.to_s)
25 all.delete(name.to_s)
26 end
26 end
27
27
28 def self.all
28 def self.all
29 @formats ||= Hash.new(Base.instance)
29 @formats ||= Hash.new(Base.instance)
30 end
30 end
31
31
32 def self.available_formats
32 def self.available_formats
33 all.keys
33 all.keys
34 end
34 end
35
35
36 def self.find(name)
36 def self.find(name)
37 all[name.to_s]
37 all[name.to_s]
38 end
38 end
39
39
40 # Return an array of custom field formats which can be used in select_tag
40 # Return an array of custom field formats which can be used in select_tag
41 def self.as_select(class_name=nil)
41 def self.as_select(class_name=nil)
42 formats = all.values.select do |format|
42 formats = all.values.select do |format|
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 end
44 end
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 end
46 end
47
47
48 class Base
48 class Base
49 include Singleton
49 include Singleton
50 include Redmine::I18n
50 include Redmine::I18n
51 include ERB::Util
51 include ERB::Util
52
52
53 class_attribute :format_name
53 class_attribute :format_name
54 self.format_name = nil
54 self.format_name = nil
55
55
56 # Set this to true if the format supports multiple values
56 # Set this to true if the format supports multiple values
57 class_attribute :multiple_supported
57 class_attribute :multiple_supported
58 self.multiple_supported = false
58 self.multiple_supported = false
59
59
60 # Set this to true if the format supports textual search on custom values
60 # Set this to true if the format supports textual search on custom values
61 class_attribute :searchable_supported
61 class_attribute :searchable_supported
62 self.searchable_supported = false
62 self.searchable_supported = false
63
63
64 # Restricts the classes that the custom field can be added to
64 # Restricts the classes that the custom field can be added to
65 # Set to nil for no restrictions
65 # Set to nil for no restrictions
66 class_attribute :customized_class_names
66 class_attribute :customized_class_names
67 self.customized_class_names = nil
67 self.customized_class_names = nil
68
68
69 # Name of the partial for editing the custom field
69 # Name of the partial for editing the custom field
70 class_attribute :form_partial
70 class_attribute :form_partial
71 self.form_partial = nil
71 self.form_partial = nil
72
72
73 class_attribute :change_as_diff
73 class_attribute :change_as_diff
74 self.change_as_diff = false
74 self.change_as_diff = false
75
75
76 def self.add(name)
76 def self.add(name)
77 self.format_name = name
77 self.format_name = name
78 Redmine::FieldFormat.add(name, self)
78 Redmine::FieldFormat.add(name, self)
79 end
79 end
80 private_class_method :add
80 private_class_method :add
81
81
82 def self.field_attributes(*args)
82 def self.field_attributes(*args)
83 CustomField.store_accessor :format_store, *args
83 CustomField.store_accessor :format_store, *args
84 end
84 end
85
85
86 field_attributes :url_pattern
86 field_attributes :url_pattern
87
87
88 def name
88 def name
89 self.class.format_name
89 self.class.format_name
90 end
90 end
91
91
92 def label
92 def label
93 "label_#{name}"
93 "label_#{name}"
94 end
94 end
95
95
96 def cast_custom_value(custom_value)
96 def cast_custom_value(custom_value)
97 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
97 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
98 end
98 end
99
99
100 def cast_value(custom_field, value, customized=nil)
100 def cast_value(custom_field, value, customized=nil)
101 if value.blank?
101 if value.blank?
102 nil
102 nil
103 elsif value.is_a?(Array)
103 elsif value.is_a?(Array)
104 casted = value.map do |v|
104 casted = value.map do |v|
105 cast_single_value(custom_field, v, customized)
105 cast_single_value(custom_field, v, customized)
106 end
106 end
107 casted.compact.sort
107 casted.compact.sort
108 else
108 else
109 cast_single_value(custom_field, value, customized)
109 cast_single_value(custom_field, value, customized)
110 end
110 end
111 end
111 end
112
112
113 def cast_single_value(custom_field, value, customized=nil)
113 def cast_single_value(custom_field, value, customized=nil)
114 value.to_s
114 value.to_s
115 end
115 end
116
116
117 def target_class
117 def target_class
118 nil
118 nil
119 end
119 end
120
120
121 def possible_custom_value_options(custom_value)
121 def possible_custom_value_options(custom_value)
122 possible_values_options(custom_value.custom_field, custom_value.customized)
122 possible_values_options(custom_value.custom_field, custom_value.customized)
123 end
123 end
124
124
125 def possible_values_options(custom_field, object=nil)
125 def possible_values_options(custom_field, object=nil)
126 []
126 []
127 end
127 end
128
128
129 # Returns the validation errors for custom_field
129 # Returns the validation errors for custom_field
130 # Should return an empty array if custom_field is valid
130 # Should return an empty array if custom_field is valid
131 def validate_custom_field(custom_field)
131 def validate_custom_field(custom_field)
132 []
132 []
133 end
133 end
134
134
135 # Returns the validation error messages for custom_value
135 # Returns the validation error messages for custom_value
136 # Should return an empty array if custom_value is valid
136 # Should return an empty array if custom_value is valid
137 def validate_custom_value(custom_value)
137 def validate_custom_value(custom_value)
138 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
138 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
139 errors = values.map do |value|
139 errors = values.map do |value|
140 validate_single_value(custom_value.custom_field, value, custom_value.customized)
140 validate_single_value(custom_value.custom_field, value, custom_value.customized)
141 end
141 end
142 errors.flatten.uniq
142 errors.flatten.uniq
143 end
143 end
144
144
145 def validate_single_value(custom_field, value, customized=nil)
145 def validate_single_value(custom_field, value, customized=nil)
146 []
146 []
147 end
147 end
148
148
149 def formatted_custom_value(view, custom_value, html=false)
149 def formatted_custom_value(view, custom_value, html=false)
150 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
150 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
151 end
151 end
152
152
153 def formatted_value(view, custom_field, value, customized=nil, html=false)
153 def formatted_value(view, custom_field, value, customized=nil, html=false)
154 casted = cast_value(custom_field, value, customized)
154 casted = cast_value(custom_field, value, customized)
155 if html && custom_field.url_pattern.present?
155 if html && custom_field.url_pattern.present?
156 texts_and_urls = Array.wrap(casted).map do |single_value|
156 texts_and_urls = Array.wrap(casted).map do |single_value|
157 text = view.format_object(single_value, false).to_s
157 text = view.format_object(single_value, false).to_s
158 url = url_from_pattern(custom_field, single_value, customized)
158 url = url_from_pattern(custom_field, single_value, customized)
159 [text, url]
159 [text, url]
160 end
160 end
161 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
161 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
162 links.join(', ').html_safe
162 links.join(', ').html_safe
163 else
163 else
164 casted
164 casted
165 end
165 end
166 end
166 end
167
167
168 # Returns an URL generated with the custom field URL pattern
168 # Returns an URL generated with the custom field URL pattern
169 # and variables substitution:
169 # and variables substitution:
170 # %value% => the custom field value
170 # %value% => the custom field value
171 # %id% => id of the customized object
171 # %id% => id of the customized object
172 # %project_id% => id of the project of the customized object if defined
172 # %project_id% => id of the project of the customized object if defined
173 # %project_identifier% => identifier of the project of the customized object if defined
173 # %project_identifier% => identifier of the project of the customized object if defined
174 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
174 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
175 def url_from_pattern(custom_field, value, customized)
175 def url_from_pattern(custom_field, value, customized)
176 url = custom_field.url_pattern.to_s.dup
176 url = custom_field.url_pattern.to_s.dup
177 url.gsub!('%value%') {value.to_s}
177 url.gsub!('%value%') {value.to_s}
178 url.gsub!('%id%') {customized.id.to_s}
178 url.gsub!('%id%') {customized.id.to_s}
179 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
179 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
180 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
180 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
181 if custom_field.regexp.present?
181 if custom_field.regexp.present?
182 url.gsub!(%r{%m(\d+)%}) do
182 url.gsub!(%r{%m(\d+)%}) do
183 m = $1.to_i
183 m = $1.to_i
184 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
184 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
185 matches[m].to_s
185 matches[m].to_s
186 end
186 end
187 end
187 end
188 end
188 end
189 url
189 url
190 end
190 end
191 protected :url_from_pattern
191 protected :url_from_pattern
192
192
193 def edit_tag(view, tag_id, tag_name, custom_value, options={})
193 def edit_tag(view, tag_id, tag_name, custom_value, options={})
194 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
194 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
195 end
195 end
196
196
197 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
197 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
198 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
198 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
199 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
199 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
200 end
200 end
201
201
202 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
202 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
203 if custom_field.is_required?
203 if custom_field.is_required?
204 ''.html_safe
204 ''.html_safe
205 else
205 else
206 view.content_tag('label',
206 view.content_tag('label',
207 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
207 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
208 :class => 'inline'
208 :class => 'inline'
209 )
209 )
210 end
210 end
211 end
211 end
212 protected :bulk_clear_tag
212 protected :bulk_clear_tag
213
213
214 def query_filter_options(custom_field, query)
214 def query_filter_options(custom_field, query)
215 {:type => :string}
215 {:type => :string}
216 end
216 end
217
217
218 def before_custom_field_save(custom_field)
218 def before_custom_field_save(custom_field)
219 end
219 end
220
220
221 # Returns a ORDER BY clause that can used to sort customized
221 # Returns a ORDER BY clause that can used to sort customized
222 # objects by their value of the custom field.
222 # objects by their value of the custom field.
223 # Returns nil if the custom field can not be used for sorting.
223 # Returns nil if the custom field can not be used for sorting.
224 def order_statement(custom_field)
224 def order_statement(custom_field)
225 # COALESCE is here to make sure that blank and NULL values are sorted equally
225 # COALESCE is here to make sure that blank and NULL values are sorted equally
226 "COALESCE(#{join_alias custom_field}.value, '')"
226 "COALESCE(#{join_alias custom_field}.value, '')"
227 end
227 end
228
228
229 # Returns a GROUP BY clause that can used to group by custom value
229 # Returns a GROUP BY clause that can used to group by custom value
230 # Returns nil if the custom field can not be used for grouping.
230 # Returns nil if the custom field can not be used for grouping.
231 def group_statement(custom_field)
231 def group_statement(custom_field)
232 nil
232 nil
233 end
233 end
234
234
235 # Returns a JOIN clause that is added to the query when sorting by custom values
235 # Returns a JOIN clause that is added to the query when sorting by custom values
236 def join_for_order_statement(custom_field)
236 def join_for_order_statement(custom_field)
237 alias_name = join_alias(custom_field)
237 alias_name = join_alias(custom_field)
238
238
239 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
239 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
240 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
240 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
241 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
241 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
242 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
242 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
243 " AND (#{custom_field.visibility_by_project_condition})" +
243 " AND (#{custom_field.visibility_by_project_condition})" +
244 " AND #{alias_name}.value <> ''" +
244 " AND #{alias_name}.value <> ''" +
245 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
245 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
246 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
246 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
247 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
247 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
248 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
248 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
249 end
249 end
250
250
251 def join_alias(custom_field)
251 def join_alias(custom_field)
252 "cf_#{custom_field.id}"
252 "cf_#{custom_field.id}"
253 end
253 end
254 protected :join_alias
254 protected :join_alias
255 end
255 end
256
256
257 class Unbounded < Base
257 class Unbounded < Base
258 def validate_single_value(custom_field, value, customized=nil)
258 def validate_single_value(custom_field, value, customized=nil)
259 errs = super
259 errs = super
260 value = value.to_s
260 value = value.to_s
261 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
261 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
262 errs << ::I18n.t('activerecord.errors.messages.invalid')
262 errs << ::I18n.t('activerecord.errors.messages.invalid')
263 end
263 end
264 if custom_field.min_length && value.length < custom_field.min_length
264 if custom_field.min_length && value.length < custom_field.min_length
265 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
265 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
266 end
266 end
267 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
267 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
268 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
268 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
269 end
269 end
270 errs
270 errs
271 end
271 end
272 end
272 end
273
273
274 class StringFormat < Unbounded
274 class StringFormat < Unbounded
275 add 'string'
275 add 'string'
276 self.searchable_supported = true
276 self.searchable_supported = true
277 self.form_partial = 'custom_fields/formats/string'
277 self.form_partial = 'custom_fields/formats/string'
278 field_attributes :text_formatting
278 field_attributes :text_formatting
279
279
280 def formatted_value(view, custom_field, value, customized=nil, html=false)
280 def formatted_value(view, custom_field, value, customized=nil, html=false)
281 if html
281 if html
282 if custom_field.url_pattern.present?
282 if custom_field.url_pattern.present?
283 super
283 super
284 elsif custom_field.text_formatting == 'full'
284 elsif custom_field.text_formatting == 'full'
285 view.textilizable(value, :object => customized)
285 view.textilizable(value, :object => customized)
286 else
286 else
287 value.to_s
287 value.to_s
288 end
288 end
289 else
289 else
290 value.to_s
290 value.to_s
291 end
291 end
292 end
292 end
293 end
293 end
294
294
295 class TextFormat < Unbounded
295 class TextFormat < Unbounded
296 add 'text'
296 add 'text'
297 self.searchable_supported = true
297 self.searchable_supported = true
298 self.form_partial = 'custom_fields/formats/text'
298 self.form_partial = 'custom_fields/formats/text'
299 self.change_as_diff = true
299 self.change_as_diff = true
300
300
301 def formatted_value(view, custom_field, value, customized=nil, html=false)
301 def formatted_value(view, custom_field, value, customized=nil, html=false)
302 if html
302 if html
303 if value.present?
303 if custom_field.text_formatting == 'full'
304 if custom_field.text_formatting == 'full'
304 view.textilizable(value, :object => customized)
305 view.textilizable(value, :object => customized)
305 else
306 else
306 view.simple_format(html_escape(value))
307 view.simple_format(html_escape(value))
307 end
308 end
308 else
309 else
310 ''
311 end
312 else
309 value.to_s
313 value.to_s
310 end
314 end
311 end
315 end
312
316
313 def edit_tag(view, tag_id, tag_name, custom_value, options={})
317 def edit_tag(view, tag_id, tag_name, custom_value, options={})
314 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
318 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
315 end
319 end
316
320
317 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
321 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
318 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
322 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
319 '<br />'.html_safe +
323 '<br />'.html_safe +
320 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
324 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
321 end
325 end
322
326
323 def query_filter_options(custom_field, query)
327 def query_filter_options(custom_field, query)
324 {:type => :text}
328 {:type => :text}
325 end
329 end
326 end
330 end
327
331
328 class LinkFormat < StringFormat
332 class LinkFormat < StringFormat
329 add 'link'
333 add 'link'
330 self.searchable_supported = false
334 self.searchable_supported = false
331 self.form_partial = 'custom_fields/formats/link'
335 self.form_partial = 'custom_fields/formats/link'
332
336
333 def formatted_value(view, custom_field, value, customized=nil, html=false)
337 def formatted_value(view, custom_field, value, customized=nil, html=false)
334 if html
338 if html
335 if custom_field.url_pattern.present?
339 if custom_field.url_pattern.present?
336 url = url_from_pattern(custom_field, value, customized)
340 url = url_from_pattern(custom_field, value, customized)
337 else
341 else
338 url = value.to_s
342 url = value.to_s
339 unless url =~ %r{\A[a-z]+://}i
343 unless url =~ %r{\A[a-z]+://}i
340 # no protocol found, use http by default
344 # no protocol found, use http by default
341 url = "http://" + url
345 url = "http://" + url
342 end
346 end
343 end
347 end
344 view.link_to value.to_s, url
348 view.link_to value.to_s, url
345 else
349 else
346 value.to_s
350 value.to_s
347 end
351 end
348 end
352 end
349 end
353 end
350
354
351 class Numeric < Unbounded
355 class Numeric < Unbounded
352 self.form_partial = 'custom_fields/formats/numeric'
356 self.form_partial = 'custom_fields/formats/numeric'
353
357
354 def order_statement(custom_field)
358 def order_statement(custom_field)
355 # Make the database cast values into numeric
359 # Make the database cast values into numeric
356 # Postgresql will raise an error if a value can not be casted!
360 # Postgresql will raise an error if a value can not be casted!
357 # CustomValue validations should ensure that it doesn't occur
361 # CustomValue validations should ensure that it doesn't occur
358 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
362 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
359 end
363 end
360 end
364 end
361
365
362 class IntFormat < Numeric
366 class IntFormat < Numeric
363 add 'int'
367 add 'int'
364
368
365 def label
369 def label
366 "label_integer"
370 "label_integer"
367 end
371 end
368
372
369 def cast_single_value(custom_field, value, customized=nil)
373 def cast_single_value(custom_field, value, customized=nil)
370 value.to_i
374 value.to_i
371 end
375 end
372
376
373 def validate_single_value(custom_field, value, customized=nil)
377 def validate_single_value(custom_field, value, customized=nil)
374 errs = super
378 errs = super
375 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
379 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
376 errs
380 errs
377 end
381 end
378
382
379 def query_filter_options(custom_field, query)
383 def query_filter_options(custom_field, query)
380 {:type => :integer}
384 {:type => :integer}
381 end
385 end
382
386
383 def group_statement(custom_field)
387 def group_statement(custom_field)
384 order_statement(custom_field)
388 order_statement(custom_field)
385 end
389 end
386 end
390 end
387
391
388 class FloatFormat < Numeric
392 class FloatFormat < Numeric
389 add 'float'
393 add 'float'
390
394
391 def cast_single_value(custom_field, value, customized=nil)
395 def cast_single_value(custom_field, value, customized=nil)
392 value.to_f
396 value.to_f
393 end
397 end
394
398
395 def validate_single_value(custom_field, value, customized=nil)
399 def validate_single_value(custom_field, value, customized=nil)
396 errs = super
400 errs = super
397 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
401 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
398 errs
402 errs
399 end
403 end
400
404
401 def query_filter_options(custom_field, query)
405 def query_filter_options(custom_field, query)
402 {:type => :float}
406 {:type => :float}
403 end
407 end
404 end
408 end
405
409
406 class DateFormat < Unbounded
410 class DateFormat < Unbounded
407 add 'date'
411 add 'date'
408 self.form_partial = 'custom_fields/formats/date'
412 self.form_partial = 'custom_fields/formats/date'
409
413
410 def cast_single_value(custom_field, value, customized=nil)
414 def cast_single_value(custom_field, value, customized=nil)
411 value.to_date rescue nil
415 value.to_date rescue nil
412 end
416 end
413
417
414 def validate_single_value(custom_field, value, customized=nil)
418 def validate_single_value(custom_field, value, customized=nil)
415 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
419 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
416 []
420 []
417 else
421 else
418 [::I18n.t('activerecord.errors.messages.not_a_date')]
422 [::I18n.t('activerecord.errors.messages.not_a_date')]
419 end
423 end
420 end
424 end
421
425
422 def edit_tag(view, tag_id, tag_name, custom_value, options={})
426 def edit_tag(view, tag_id, tag_name, custom_value, options={})
423 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
427 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
424 view.calendar_for(tag_id)
428 view.calendar_for(tag_id)
425 end
429 end
426
430
427 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
431 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
428 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
432 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
429 view.calendar_for(tag_id) +
433 view.calendar_for(tag_id) +
430 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
434 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
431 end
435 end
432
436
433 def query_filter_options(custom_field, query)
437 def query_filter_options(custom_field, query)
434 {:type => :date}
438 {:type => :date}
435 end
439 end
436
440
437 def group_statement(custom_field)
441 def group_statement(custom_field)
438 order_statement(custom_field)
442 order_statement(custom_field)
439 end
443 end
440 end
444 end
441
445
442 class List < Base
446 class List < Base
443 self.multiple_supported = true
447 self.multiple_supported = true
444 field_attributes :edit_tag_style
448 field_attributes :edit_tag_style
445
449
446 def edit_tag(view, tag_id, tag_name, custom_value, options={})
450 def edit_tag(view, tag_id, tag_name, custom_value, options={})
447 if custom_value.custom_field.edit_tag_style == 'check_box'
451 if custom_value.custom_field.edit_tag_style == 'check_box'
448 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
452 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
449 else
453 else
450 select_edit_tag(view, tag_id, tag_name, custom_value, options)
454 select_edit_tag(view, tag_id, tag_name, custom_value, options)
451 end
455 end
452 end
456 end
453
457
454 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
458 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
455 opts = []
459 opts = []
456 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
460 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
457 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
461 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
458 opts += possible_values_options(custom_field, objects)
462 opts += possible_values_options(custom_field, objects)
459 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
463 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
460 end
464 end
461
465
462 def query_filter_options(custom_field, query)
466 def query_filter_options(custom_field, query)
463 {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
467 {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
464 end
468 end
465
469
466 protected
470 protected
467
471
468 # Renders the edit tag as a select tag
472 # Renders the edit tag as a select tag
469 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
473 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
470 blank_option = ''.html_safe
474 blank_option = ''.html_safe
471 unless custom_value.custom_field.multiple?
475 unless custom_value.custom_field.multiple?
472 if custom_value.custom_field.is_required?
476 if custom_value.custom_field.is_required?
473 unless custom_value.custom_field.default_value.present?
477 unless custom_value.custom_field.default_value.present?
474 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
478 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
475 end
479 end
476 else
480 else
477 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
481 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
478 end
482 end
479 end
483 end
480 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
484 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
481 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
485 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
482 if custom_value.custom_field.multiple?
486 if custom_value.custom_field.multiple?
483 s << view.hidden_field_tag(tag_name, '')
487 s << view.hidden_field_tag(tag_name, '')
484 end
488 end
485 s
489 s
486 end
490 end
487
491
488 # Renders the edit tag as check box or radio tags
492 # Renders the edit tag as check box or radio tags
489 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
493 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
490 opts = []
494 opts = []
491 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
495 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
492 opts << ["(#{l(:label_none)})", '']
496 opts << ["(#{l(:label_none)})", '']
493 end
497 end
494 opts += possible_custom_value_options(custom_value)
498 opts += possible_custom_value_options(custom_value)
495 s = ''.html_safe
499 s = ''.html_safe
496 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
500 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
497 opts.each do |label, value|
501 opts.each do |label, value|
498 value ||= label
502 value ||= label
499 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
503 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
500 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
504 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
501 # set the id on the first tag only
505 # set the id on the first tag only
502 tag_id = nil
506 tag_id = nil
503 s << view.content_tag('label', tag + ' ' + label)
507 s << view.content_tag('label', tag + ' ' + label)
504 end
508 end
505 if custom_value.custom_field.multiple?
509 if custom_value.custom_field.multiple?
506 s << view.hidden_field_tag(tag_name, '')
510 s << view.hidden_field_tag(tag_name, '')
507 end
511 end
508 css = "#{options[:class]} check_box_group"
512 css = "#{options[:class]} check_box_group"
509 view.content_tag('span', s, options.merge(:class => css))
513 view.content_tag('span', s, options.merge(:class => css))
510 end
514 end
511 end
515 end
512
516
513 class ListFormat < List
517 class ListFormat < List
514 add 'list'
518 add 'list'
515 self.searchable_supported = true
519 self.searchable_supported = true
516 self.form_partial = 'custom_fields/formats/list'
520 self.form_partial = 'custom_fields/formats/list'
517
521
518 def possible_custom_value_options(custom_value)
522 def possible_custom_value_options(custom_value)
519 options = possible_values_options(custom_value.custom_field)
523 options = possible_values_options(custom_value.custom_field)
520 missing = [custom_value.value].flatten.reject(&:blank?) - options
524 missing = [custom_value.value].flatten.reject(&:blank?) - options
521 if missing.any?
525 if missing.any?
522 options += missing
526 options += missing
523 end
527 end
524 options
528 options
525 end
529 end
526
530
527 def possible_values_options(custom_field, object=nil)
531 def possible_values_options(custom_field, object=nil)
528 custom_field.possible_values
532 custom_field.possible_values
529 end
533 end
530
534
531 def validate_custom_field(custom_field)
535 def validate_custom_field(custom_field)
532 errors = []
536 errors = []
533 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
537 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
534 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
538 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
535 errors
539 errors
536 end
540 end
537
541
538 def validate_custom_value(custom_value)
542 def validate_custom_value(custom_value)
539 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
543 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
540 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
544 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
541 if invalid_values.any?
545 if invalid_values.any?
542 [::I18n.t('activerecord.errors.messages.inclusion')]
546 [::I18n.t('activerecord.errors.messages.inclusion')]
543 else
547 else
544 []
548 []
545 end
549 end
546 end
550 end
547
551
548 def group_statement(custom_field)
552 def group_statement(custom_field)
549 order_statement(custom_field)
553 order_statement(custom_field)
550 end
554 end
551 end
555 end
552
556
553 class BoolFormat < List
557 class BoolFormat < List
554 add 'bool'
558 add 'bool'
555 self.multiple_supported = false
559 self.multiple_supported = false
556 self.form_partial = 'custom_fields/formats/bool'
560 self.form_partial = 'custom_fields/formats/bool'
557
561
558 def label
562 def label
559 "label_boolean"
563 "label_boolean"
560 end
564 end
561
565
562 def cast_single_value(custom_field, value, customized=nil)
566 def cast_single_value(custom_field, value, customized=nil)
563 value == '1' ? true : false
567 value == '1' ? true : false
564 end
568 end
565
569
566 def possible_values_options(custom_field, object=nil)
570 def possible_values_options(custom_field, object=nil)
567 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
571 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
568 end
572 end
569
573
570 def group_statement(custom_field)
574 def group_statement(custom_field)
571 order_statement(custom_field)
575 order_statement(custom_field)
572 end
576 end
573
577
574 def edit_tag(view, tag_id, tag_name, custom_value, options={})
578 def edit_tag(view, tag_id, tag_name, custom_value, options={})
575 case custom_value.custom_field.edit_tag_style
579 case custom_value.custom_field.edit_tag_style
576 when 'check_box'
580 when 'check_box'
577 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
581 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
578 when 'radio'
582 when 'radio'
579 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
583 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
580 else
584 else
581 select_edit_tag(view, tag_id, tag_name, custom_value, options)
585 select_edit_tag(view, tag_id, tag_name, custom_value, options)
582 end
586 end
583 end
587 end
584
588
585 # Renders the edit tag as a simple check box
589 # Renders the edit tag as a simple check box
586 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
590 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
587 s = ''.html_safe
591 s = ''.html_safe
588 s << view.hidden_field_tag(tag_name, '0', :id => nil)
592 s << view.hidden_field_tag(tag_name, '0', :id => nil)
589 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
593 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
590 view.content_tag('span', s, options)
594 view.content_tag('span', s, options)
591 end
595 end
592 end
596 end
593
597
594 class RecordList < List
598 class RecordList < List
595 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
599 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
596
600
597 def cast_single_value(custom_field, value, customized=nil)
601 def cast_single_value(custom_field, value, customized=nil)
598 target_class.find_by_id(value.to_i) if value.present?
602 target_class.find_by_id(value.to_i) if value.present?
599 end
603 end
600
604
601 def target_class
605 def target_class
602 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
606 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
603 end
607 end
604
608
605 def reset_target_class
609 def reset_target_class
606 @target_class = nil
610 @target_class = nil
607 end
611 end
608
612
609 def possible_custom_value_options(custom_value)
613 def possible_custom_value_options(custom_value)
610 options = possible_values_options(custom_value.custom_field, custom_value.customized)
614 options = possible_values_options(custom_value.custom_field, custom_value.customized)
611 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
615 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
612 if missing.any?
616 if missing.any?
613 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
617 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
614 options.sort_by!(&:first)
618 options.sort_by!(&:first)
615 end
619 end
616 options
620 options
617 end
621 end
618
622
619 def order_statement(custom_field)
623 def order_statement(custom_field)
620 if target_class.respond_to?(:fields_for_order_statement)
624 if target_class.respond_to?(:fields_for_order_statement)
621 target_class.fields_for_order_statement(value_join_alias(custom_field))
625 target_class.fields_for_order_statement(value_join_alias(custom_field))
622 end
626 end
623 end
627 end
624
628
625 def group_statement(custom_field)
629 def group_statement(custom_field)
626 "COALESCE(#{join_alias custom_field}.value, '')"
630 "COALESCE(#{join_alias custom_field}.value, '')"
627 end
631 end
628
632
629 def join_for_order_statement(custom_field)
633 def join_for_order_statement(custom_field)
630 alias_name = join_alias(custom_field)
634 alias_name = join_alias(custom_field)
631
635
632 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
636 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
633 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
637 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
634 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
638 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
635 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
639 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
636 " AND (#{custom_field.visibility_by_project_condition})" +
640 " AND (#{custom_field.visibility_by_project_condition})" +
637 " AND #{alias_name}.value <> ''" +
641 " AND #{alias_name}.value <> ''" +
638 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
642 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
639 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
643 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
640 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
644 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
641 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
645 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
642 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
646 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
643 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
647 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
644 end
648 end
645
649
646 def value_join_alias(custom_field)
650 def value_join_alias(custom_field)
647 join_alias(custom_field) + "_" + custom_field.field_format
651 join_alias(custom_field) + "_" + custom_field.field_format
648 end
652 end
649 protected :value_join_alias
653 protected :value_join_alias
650 end
654 end
651
655
652 class UserFormat < RecordList
656 class UserFormat < RecordList
653 add 'user'
657 add 'user'
654 self.form_partial = 'custom_fields/formats/user'
658 self.form_partial = 'custom_fields/formats/user'
655 field_attributes :user_role
659 field_attributes :user_role
656
660
657 def possible_values_options(custom_field, object=nil)
661 def possible_values_options(custom_field, object=nil)
658 if object.is_a?(Array)
662 if object.is_a?(Array)
659 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
663 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
660 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
664 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
661 elsif object.respond_to?(:project) && object.project
665 elsif object.respond_to?(:project) && object.project
662 scope = object.project.users
666 scope = object.project.users
663 if custom_field.user_role.is_a?(Array)
667 if custom_field.user_role.is_a?(Array)
664 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
668 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
665 if role_ids.any?
669 if role_ids.any?
666 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
670 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
667 end
671 end
668 end
672 end
669 scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
673 scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
670 else
674 else
671 []
675 []
672 end
676 end
673 end
677 end
674
678
675 def before_custom_field_save(custom_field)
679 def before_custom_field_save(custom_field)
676 super
680 super
677 if custom_field.user_role.is_a?(Array)
681 if custom_field.user_role.is_a?(Array)
678 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
682 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
679 end
683 end
680 end
684 end
681 end
685 end
682
686
683 class VersionFormat < RecordList
687 class VersionFormat < RecordList
684 add 'version'
688 add 'version'
685 self.form_partial = 'custom_fields/formats/version'
689 self.form_partial = 'custom_fields/formats/version'
686 field_attributes :version_status
690 field_attributes :version_status
687
691
688 def possible_values_options(custom_field, object=nil)
692 def possible_values_options(custom_field, object=nil)
689 if object.is_a?(Array)
693 if object.is_a?(Array)
690 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
694 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
691 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
695 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
692 elsif object.respond_to?(:project) && object.project
696 elsif object.respond_to?(:project) && object.project
693 scope = object.project.shared_versions
697 scope = object.project.shared_versions
694 if custom_field.version_status.is_a?(Array)
698 if custom_field.version_status.is_a?(Array)
695 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
699 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
696 if statuses.any?
700 if statuses.any?
697 scope = scope.where(:status => statuses.map(&:to_s))
701 scope = scope.where(:status => statuses.map(&:to_s))
698 end
702 end
699 end
703 end
700 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
704 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
701 else
705 else
702 []
706 []
703 end
707 end
704 end
708 end
705
709
706 def before_custom_field_save(custom_field)
710 def before_custom_field_save(custom_field)
707 super
711 super
708 if custom_field.version_status.is_a?(Array)
712 if custom_field.version_status.is_a?(Array)
709 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
713 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
710 end
714 end
711 end
715 end
712 end
716 end
713 end
717 end
714 end
718 end
@@ -1,665 +1,674
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 ProjectsControllerTest < ActionController::TestCase
20 class ProjectsControllerTest < ActionController::TestCase
21 fixtures :projects, :versions, :users, :email_addresses, :roles, :members,
21 fixtures :projects, :versions, :users, :email_addresses, :roles, :members,
22 :member_roles, :issues, :journals, :journal_details,
22 :member_roles, :issues, :journals, :journal_details,
23 :trackers, :projects_trackers, :issue_statuses,
23 :trackers, :projects_trackers, :issue_statuses,
24 :enabled_modules, :enumerations, :boards, :messages,
24 :enabled_modules, :enumerations, :boards, :messages,
25 :attachments, :custom_fields, :custom_values, :time_entries,
25 :attachments, :custom_fields, :custom_values, :time_entries,
26 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
26 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
27
27
28 def setup
28 def setup
29 @request.session[:user_id] = nil
29 @request.session[:user_id] = nil
30 Setting.default_language = 'en'
30 Setting.default_language = 'en'
31 end
31 end
32
32
33 def test_index_by_anonymous_should_not_show_private_projects
33 def test_index_by_anonymous_should_not_show_private_projects
34 get :index
34 get :index
35 assert_response :success
35 assert_response :success
36 assert_template 'index'
36 assert_template 'index'
37 projects = assigns(:projects)
37 projects = assigns(:projects)
38 assert_not_nil projects
38 assert_not_nil projects
39 assert projects.all?(&:is_public?)
39 assert projects.all?(&:is_public?)
40
40
41 assert_select 'ul' do
41 assert_select 'ul' do
42 assert_select 'li' do
42 assert_select 'li' do
43 assert_select 'a', :text => 'eCookbook'
43 assert_select 'a', :text => 'eCookbook'
44 assert_select 'ul' do
44 assert_select 'ul' do
45 assert_select 'a', :text => 'Child of private child'
45 assert_select 'a', :text => 'Child of private child'
46 end
46 end
47 end
47 end
48 end
48 end
49 assert_select 'a', :text => /Private child of eCookbook/, :count => 0
49 assert_select 'a', :text => /Private child of eCookbook/, :count => 0
50 end
50 end
51
51
52 def test_index_atom
52 def test_index_atom
53 get :index, :format => 'atom'
53 get :index, :format => 'atom'
54 assert_response :success
54 assert_response :success
55 assert_template 'common/feed'
55 assert_template 'common/feed'
56 assert_select 'feed>title', :text => 'Redmine: Latest projects'
56 assert_select 'feed>title', :text => 'Redmine: Latest projects'
57 assert_select 'feed>entry', :count => Project.visible(User.current).count
57 assert_select 'feed>entry', :count => Project.visible(User.current).count
58 end
58 end
59
59
60 test "#index by non-admin user with view_time_entries permission should show overall spent time link" do
60 test "#index by non-admin user with view_time_entries permission should show overall spent time link" do
61 @request.session[:user_id] = 3
61 @request.session[:user_id] = 3
62 get :index
62 get :index
63 assert_template 'index'
63 assert_template 'index'
64 assert_select 'a[href=?]', '/time_entries'
64 assert_select 'a[href=?]', '/time_entries'
65 end
65 end
66
66
67 test "#index by non-admin user without view_time_entries permission should not show overall spent time link" do
67 test "#index by non-admin user without view_time_entries permission should not show overall spent time link" do
68 Role.find(2).remove_permission! :view_time_entries
68 Role.find(2).remove_permission! :view_time_entries
69 Role.non_member.remove_permission! :view_time_entries
69 Role.non_member.remove_permission! :view_time_entries
70 Role.anonymous.remove_permission! :view_time_entries
70 Role.anonymous.remove_permission! :view_time_entries
71 @request.session[:user_id] = 3
71 @request.session[:user_id] = 3
72
72
73 get :index
73 get :index
74 assert_template 'index'
74 assert_template 'index'
75 assert_select 'a[href=?]', '/time_entries', 0
75 assert_select 'a[href=?]', '/time_entries', 0
76 end
76 end
77
77
78 test "#index by non-admin user with permission should show add project link" do
78 test "#index by non-admin user with permission should show add project link" do
79 Role.find(1).add_permission! :add_project
79 Role.find(1).add_permission! :add_project
80 @request.session[:user_id] = 2
80 @request.session[:user_id] = 2
81 get :index
81 get :index
82 assert_template 'index'
82 assert_template 'index'
83 assert_select 'a[href=?]', '/projects/new'
83 assert_select 'a[href=?]', '/projects/new'
84 end
84 end
85
85
86 test "#new by admin user should accept get" do
86 test "#new by admin user should accept get" do
87 @request.session[:user_id] = 1
87 @request.session[:user_id] = 1
88
88
89 get :new
89 get :new
90 assert_response :success
90 assert_response :success
91 assert_template 'new'
91 assert_template 'new'
92 end
92 end
93
93
94 test "#new by non-admin user with add_project permission should accept get" do
94 test "#new by non-admin user with add_project permission should accept get" do
95 Role.non_member.add_permission! :add_project
95 Role.non_member.add_permission! :add_project
96 @request.session[:user_id] = 9
96 @request.session[:user_id] = 9
97
97
98 get :new
98 get :new
99 assert_response :success
99 assert_response :success
100 assert_template 'new'
100 assert_template 'new'
101 assert_select 'select[name=?]', 'project[parent_id]', 0
101 assert_select 'select[name=?]', 'project[parent_id]', 0
102 end
102 end
103
103
104 test "#new by non-admin user with add_subprojects permission should accept get" do
104 test "#new by non-admin user with add_subprojects permission should accept get" do
105 Role.find(1).remove_permission! :add_project
105 Role.find(1).remove_permission! :add_project
106 Role.find(1).add_permission! :add_subprojects
106 Role.find(1).add_permission! :add_subprojects
107 @request.session[:user_id] = 2
107 @request.session[:user_id] = 2
108
108
109 get :new, :parent_id => 'ecookbook'
109 get :new, :parent_id => 'ecookbook'
110 assert_response :success
110 assert_response :success
111 assert_template 'new'
111 assert_template 'new'
112
112
113 assert_select 'select[name=?]', 'project[parent_id]' do
113 assert_select 'select[name=?]', 'project[parent_id]' do
114 # parent project selected
114 # parent project selected
115 assert_select 'option[value="1"][selected=selected]'
115 assert_select 'option[value="1"][selected=selected]'
116 # no empty value
116 # no empty value
117 assert_select 'option[value=""]', 0
117 assert_select 'option[value=""]', 0
118 end
118 end
119 end
119 end
120
120
121 test "#create by admin user should create a new project" do
121 test "#create by admin user should create a new project" do
122 @request.session[:user_id] = 1
122 @request.session[:user_id] = 1
123
123
124 post :create,
124 post :create,
125 :project => {
125 :project => {
126 :name => "blog",
126 :name => "blog",
127 :description => "weblog",
127 :description => "weblog",
128 :homepage => 'http://weblog',
128 :homepage => 'http://weblog',
129 :identifier => "blog",
129 :identifier => "blog",
130 :is_public => 1,
130 :is_public => 1,
131 :custom_field_values => { '3' => 'Beta' },
131 :custom_field_values => { '3' => 'Beta' },
132 :tracker_ids => ['1', '3'],
132 :tracker_ids => ['1', '3'],
133 # an issue custom field that is not for all project
133 # an issue custom field that is not for all project
134 :issue_custom_field_ids => ['9'],
134 :issue_custom_field_ids => ['9'],
135 :enabled_module_names => ['issue_tracking', 'news', 'repository']
135 :enabled_module_names => ['issue_tracking', 'news', 'repository']
136 }
136 }
137 assert_redirected_to '/projects/blog/settings'
137 assert_redirected_to '/projects/blog/settings'
138
138
139 project = Project.find_by_name('blog')
139 project = Project.find_by_name('blog')
140 assert_kind_of Project, project
140 assert_kind_of Project, project
141 assert project.active?
141 assert project.active?
142 assert_equal 'weblog', project.description
142 assert_equal 'weblog', project.description
143 assert_equal 'http://weblog', project.homepage
143 assert_equal 'http://weblog', project.homepage
144 assert_equal true, project.is_public?
144 assert_equal true, project.is_public?
145 assert_nil project.parent
145 assert_nil project.parent
146 assert_equal 'Beta', project.custom_value_for(3).value
146 assert_equal 'Beta', project.custom_value_for(3).value
147 assert_equal [1, 3], project.trackers.map(&:id).sort
147 assert_equal [1, 3], project.trackers.map(&:id).sort
148 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
148 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
149 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
149 assert project.issue_custom_fields.include?(IssueCustomField.find(9))
150 end
150 end
151
151
152 test "#create by admin user should create a new subproject" do
152 test "#create by admin user should create a new subproject" do
153 @request.session[:user_id] = 1
153 @request.session[:user_id] = 1
154
154
155 assert_difference 'Project.count' do
155 assert_difference 'Project.count' do
156 post :create, :project => { :name => "blog",
156 post :create, :project => { :name => "blog",
157 :description => "weblog",
157 :description => "weblog",
158 :identifier => "blog",
158 :identifier => "blog",
159 :is_public => 1,
159 :is_public => 1,
160 :custom_field_values => { '3' => 'Beta' },
160 :custom_field_values => { '3' => 'Beta' },
161 :parent_id => 1
161 :parent_id => 1
162 }
162 }
163 assert_redirected_to '/projects/blog/settings'
163 assert_redirected_to '/projects/blog/settings'
164 end
164 end
165
165
166 project = Project.find_by_name('blog')
166 project = Project.find_by_name('blog')
167 assert_kind_of Project, project
167 assert_kind_of Project, project
168 assert_equal Project.find(1), project.parent
168 assert_equal Project.find(1), project.parent
169 end
169 end
170
170
171 test "#create by admin user should continue" do
171 test "#create by admin user should continue" do
172 @request.session[:user_id] = 1
172 @request.session[:user_id] = 1
173
173
174 assert_difference 'Project.count' do
174 assert_difference 'Project.count' do
175 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
175 post :create, :project => {:name => "blog", :identifier => "blog"}, :continue => 'Create and continue'
176 end
176 end
177 assert_redirected_to '/projects/new'
177 assert_redirected_to '/projects/new'
178 end
178 end
179
179
180 test "#create by non-admin user with add_project permission should create a new project" do
180 test "#create by non-admin user with add_project permission should create a new project" do
181 Role.non_member.add_permission! :add_project
181 Role.non_member.add_permission! :add_project
182 @request.session[:user_id] = 9
182 @request.session[:user_id] = 9
183
183
184 post :create, :project => { :name => "blog",
184 post :create, :project => { :name => "blog",
185 :description => "weblog",
185 :description => "weblog",
186 :identifier => "blog",
186 :identifier => "blog",
187 :is_public => 1,
187 :is_public => 1,
188 :custom_field_values => { '3' => 'Beta' },
188 :custom_field_values => { '3' => 'Beta' },
189 :tracker_ids => ['1', '3'],
189 :tracker_ids => ['1', '3'],
190 :enabled_module_names => ['issue_tracking', 'news', 'repository']
190 :enabled_module_names => ['issue_tracking', 'news', 'repository']
191 }
191 }
192
192
193 assert_redirected_to '/projects/blog/settings'
193 assert_redirected_to '/projects/blog/settings'
194
194
195 project = Project.find_by_name('blog')
195 project = Project.find_by_name('blog')
196 assert_kind_of Project, project
196 assert_kind_of Project, project
197 assert_equal 'weblog', project.description
197 assert_equal 'weblog', project.description
198 assert_equal true, project.is_public?
198 assert_equal true, project.is_public?
199 assert_equal [1, 3], project.trackers.map(&:id).sort
199 assert_equal [1, 3], project.trackers.map(&:id).sort
200 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
200 assert_equal ['issue_tracking', 'news', 'repository'], project.enabled_module_names.sort
201
201
202 # User should be added as a project member
202 # User should be added as a project member
203 assert User.find(9).member_of?(project)
203 assert User.find(9).member_of?(project)
204 assert_equal 1, project.members.size
204 assert_equal 1, project.members.size
205 end
205 end
206
206
207 test "#create by non-admin user with add_project permission should fail with parent_id" do
207 test "#create by non-admin user with add_project permission should fail with parent_id" do
208 Role.non_member.add_permission! :add_project
208 Role.non_member.add_permission! :add_project
209 @request.session[:user_id] = 9
209 @request.session[:user_id] = 9
210
210
211 assert_no_difference 'Project.count' do
211 assert_no_difference 'Project.count' do
212 post :create, :project => { :name => "blog",
212 post :create, :project => { :name => "blog",
213 :description => "weblog",
213 :description => "weblog",
214 :identifier => "blog",
214 :identifier => "blog",
215 :is_public => 1,
215 :is_public => 1,
216 :custom_field_values => { '3' => 'Beta' },
216 :custom_field_values => { '3' => 'Beta' },
217 :parent_id => 1
217 :parent_id => 1
218 }
218 }
219 end
219 end
220 assert_response :success
220 assert_response :success
221 project = assigns(:project)
221 project = assigns(:project)
222 assert_kind_of Project, project
222 assert_kind_of Project, project
223 assert_not_equal [], project.errors[:parent_id]
223 assert_not_equal [], project.errors[:parent_id]
224 end
224 end
225
225
226 test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do
226 test "#create by non-admin user with add_subprojects permission should create a project with a parent_id" do
227 Role.find(1).remove_permission! :add_project
227 Role.find(1).remove_permission! :add_project
228 Role.find(1).add_permission! :add_subprojects
228 Role.find(1).add_permission! :add_subprojects
229 @request.session[:user_id] = 2
229 @request.session[:user_id] = 2
230
230
231 post :create, :project => { :name => "blog",
231 post :create, :project => { :name => "blog",
232 :description => "weblog",
232 :description => "weblog",
233 :identifier => "blog",
233 :identifier => "blog",
234 :is_public => 1,
234 :is_public => 1,
235 :custom_field_values => { '3' => 'Beta' },
235 :custom_field_values => { '3' => 'Beta' },
236 :parent_id => 1
236 :parent_id => 1
237 }
237 }
238 assert_redirected_to '/projects/blog/settings'
238 assert_redirected_to '/projects/blog/settings'
239 project = Project.find_by_name('blog')
239 project = Project.find_by_name('blog')
240 end
240 end
241
241
242 test "#create by non-admin user with add_subprojects permission should fail without parent_id" do
242 test "#create by non-admin user with add_subprojects permission should fail without parent_id" do
243 Role.find(1).remove_permission! :add_project
243 Role.find(1).remove_permission! :add_project
244 Role.find(1).add_permission! :add_subprojects
244 Role.find(1).add_permission! :add_subprojects
245 @request.session[:user_id] = 2
245 @request.session[:user_id] = 2
246
246
247 assert_no_difference 'Project.count' do
247 assert_no_difference 'Project.count' do
248 post :create, :project => { :name => "blog",
248 post :create, :project => { :name => "blog",
249 :description => "weblog",
249 :description => "weblog",
250 :identifier => "blog",
250 :identifier => "blog",
251 :is_public => 1,
251 :is_public => 1,
252 :custom_field_values => { '3' => 'Beta' }
252 :custom_field_values => { '3' => 'Beta' }
253 }
253 }
254 end
254 end
255 assert_response :success
255 assert_response :success
256 project = assigns(:project)
256 project = assigns(:project)
257 assert_kind_of Project, project
257 assert_kind_of Project, project
258 assert_not_equal [], project.errors[:parent_id]
258 assert_not_equal [], project.errors[:parent_id]
259 end
259 end
260
260
261 test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do
261 test "#create by non-admin user with add_subprojects permission should fail with unauthorized parent_id" do
262 Role.find(1).remove_permission! :add_project
262 Role.find(1).remove_permission! :add_project
263 Role.find(1).add_permission! :add_subprojects
263 Role.find(1).add_permission! :add_subprojects
264 @request.session[:user_id] = 2
264 @request.session[:user_id] = 2
265
265
266 assert !User.find(2).member_of?(Project.find(6))
266 assert !User.find(2).member_of?(Project.find(6))
267 assert_no_difference 'Project.count' do
267 assert_no_difference 'Project.count' do
268 post :create, :project => { :name => "blog",
268 post :create, :project => { :name => "blog",
269 :description => "weblog",
269 :description => "weblog",
270 :identifier => "blog",
270 :identifier => "blog",
271 :is_public => 1,
271 :is_public => 1,
272 :custom_field_values => { '3' => 'Beta' },
272 :custom_field_values => { '3' => 'Beta' },
273 :parent_id => 6
273 :parent_id => 6
274 }
274 }
275 end
275 end
276 assert_response :success
276 assert_response :success
277 project = assigns(:project)
277 project = assigns(:project)
278 assert_kind_of Project, project
278 assert_kind_of Project, project
279 assert_not_equal [], project.errors[:parent_id]
279 assert_not_equal [], project.errors[:parent_id]
280 end
280 end
281
281
282 def test_create_subproject_with_inherit_members_should_inherit_members
282 def test_create_subproject_with_inherit_members_should_inherit_members
283 Role.find_by_name('Manager').add_permission! :add_subprojects
283 Role.find_by_name('Manager').add_permission! :add_subprojects
284 parent = Project.find(1)
284 parent = Project.find(1)
285 @request.session[:user_id] = 2
285 @request.session[:user_id] = 2
286
286
287 assert_difference 'Project.count' do
287 assert_difference 'Project.count' do
288 post :create, :project => {
288 post :create, :project => {
289 :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1'
289 :name => 'inherited', :identifier => 'inherited', :parent_id => parent.id, :inherit_members => '1'
290 }
290 }
291 assert_response 302
291 assert_response 302
292 end
292 end
293
293
294 project = Project.order('id desc').first
294 project = Project.order('id desc').first
295 assert_equal 'inherited', project.name
295 assert_equal 'inherited', project.name
296 assert_equal parent, project.parent
296 assert_equal parent, project.parent
297 assert project.memberships.count > 0
297 assert project.memberships.count > 0
298 assert_equal parent.memberships.count, project.memberships.count
298 assert_equal parent.memberships.count, project.memberships.count
299 end
299 end
300
300
301 def test_create_should_preserve_modules_on_validation_failure
301 def test_create_should_preserve_modules_on_validation_failure
302 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
302 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
303 @request.session[:user_id] = 1
303 @request.session[:user_id] = 1
304 assert_no_difference 'Project.count' do
304 assert_no_difference 'Project.count' do
305 post :create, :project => {
305 post :create, :project => {
306 :name => "blog",
306 :name => "blog",
307 :identifier => "",
307 :identifier => "",
308 :enabled_module_names => %w(issue_tracking news)
308 :enabled_module_names => %w(issue_tracking news)
309 }
309 }
310 end
310 end
311 assert_response :success
311 assert_response :success
312 project = assigns(:project)
312 project = assigns(:project)
313 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
313 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
314 end
314 end
315 end
315 end
316
316
317 def test_show_by_id
317 def test_show_by_id
318 get :show, :id => 1
318 get :show, :id => 1
319 assert_response :success
319 assert_response :success
320 assert_template 'show'
320 assert_template 'show'
321 assert_not_nil assigns(:project)
321 assert_not_nil assigns(:project)
322 end
322 end
323
323
324 def test_show_by_identifier
324 def test_show_by_identifier
325 get :show, :id => 'ecookbook'
325 get :show, :id => 'ecookbook'
326 assert_response :success
326 assert_response :success
327 assert_template 'show'
327 assert_template 'show'
328 assert_not_nil assigns(:project)
328 assert_not_nil assigns(:project)
329 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
329 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
330
330
331 assert_select 'li', :text => /Development status/
331 assert_select 'li', :text => /Development status/
332 end
332 end
333
333
334 def test_show_should_not_display_empty_sidebar
334 def test_show_should_not_display_empty_sidebar
335 p = Project.find(1)
335 p = Project.find(1)
336 p.enabled_module_names = []
336 p.enabled_module_names = []
337 p.save!
337 p.save!
338
338
339 get :show, :id => 'ecookbook'
339 get :show, :id => 'ecookbook'
340 assert_response :success
340 assert_response :success
341 assert_select '#main.nosidebar'
341 assert_select '#main.nosidebar'
342 end
342 end
343
343
344 def test_show_should_not_display_hidden_custom_fields
344 def test_show_should_not_display_hidden_custom_fields
345 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
345 ProjectCustomField.find_by_name('Development status').update_attribute :visible, false
346 get :show, :id => 'ecookbook'
346 get :show, :id => 'ecookbook'
347 assert_response :success
347 assert_response :success
348 assert_template 'show'
348 assert_template 'show'
349 assert_not_nil assigns(:project)
349 assert_not_nil assigns(:project)
350
350
351 assert_select 'li', :text => /Development status/, :count => 0
351 assert_select 'li', :text => /Development status/, :count => 0
352 end
352 end
353
353
354 def test_show_should_not_display_blank_custom_fields_with_multiple_values
354 def test_show_should_not_display_blank_custom_fields_with_multiple_values
355 f1 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Foo Bar), :multiple => true
355 f1 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Foo Bar), :multiple => true
356 f2 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Baz Qux), :multiple => true
356 f2 = ProjectCustomField.generate! :field_format => 'list', :possible_values => %w(Baz Qux), :multiple => true
357 project = Project.generate!(:custom_field_values => {f2.id.to_s => %w(Qux)})
357 project = Project.generate!(:custom_field_values => {f2.id.to_s => %w(Qux)})
358
358
359 get :show, :id => project.id
359 get :show, :id => project.id
360 assert_response :success
360 assert_response :success
361
361
362 assert_select 'li', :text => /#{f1.name}/, :count => 0
362 assert_select 'li', :text => /#{f1.name}/, :count => 0
363 assert_select 'li', :text => /#{f2.name}/
363 assert_select 'li', :text => /#{f2.name}/
364 end
364 end
365
365
366 def test_show_should_not_display_blank_text_custom_fields
367 f1 = ProjectCustomField.generate! :field_format => 'text'
368
369 get :show, :id => 1
370 assert_response :success
371
372 assert_select 'li', :text => /#{f1.name}/, :count => 0
373 end
374
366 def test_show_should_not_fail_when_custom_values_are_nil
375 def test_show_should_not_fail_when_custom_values_are_nil
367 project = Project.find_by_identifier('ecookbook')
376 project = Project.find_by_identifier('ecookbook')
368 project.custom_values.first.update_attribute(:value, nil)
377 project.custom_values.first.update_attribute(:value, nil)
369 get :show, :id => 'ecookbook'
378 get :show, :id => 'ecookbook'
370 assert_response :success
379 assert_response :success
371 assert_template 'show'
380 assert_template 'show'
372 assert_not_nil assigns(:project)
381 assert_not_nil assigns(:project)
373 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
382 assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
374 end
383 end
375
384
376 def show_archived_project_should_be_denied
385 def show_archived_project_should_be_denied
377 project = Project.find_by_identifier('ecookbook')
386 project = Project.find_by_identifier('ecookbook')
378 project.archive!
387 project.archive!
379
388
380 get :show, :id => 'ecookbook'
389 get :show, :id => 'ecookbook'
381 assert_response 403
390 assert_response 403
382 assert_nil assigns(:project)
391 assert_nil assigns(:project)
383 assert_select 'p', :text => /archived/
392 assert_select 'p', :text => /archived/
384 end
393 end
385
394
386 def test_show_should_not_show_private_subprojects_that_are_not_visible
395 def test_show_should_not_show_private_subprojects_that_are_not_visible
387 get :show, :id => 'ecookbook'
396 get :show, :id => 'ecookbook'
388 assert_response :success
397 assert_response :success
389 assert_template 'show'
398 assert_template 'show'
390 assert_select 'a', :text => /Private child/, :count => 0
399 assert_select 'a', :text => /Private child/, :count => 0
391 end
400 end
392
401
393 def test_show_should_show_private_subprojects_that_are_visible
402 def test_show_should_show_private_subprojects_that_are_visible
394 @request.session[:user_id] = 2 # manager who is a member of the private subproject
403 @request.session[:user_id] = 2 # manager who is a member of the private subproject
395 get :show, :id => 'ecookbook'
404 get :show, :id => 'ecookbook'
396 assert_response :success
405 assert_response :success
397 assert_template 'show'
406 assert_template 'show'
398 assert_select 'a', :text => /Private child/
407 assert_select 'a', :text => /Private child/
399 end
408 end
400
409
401 def test_settings
410 def test_settings
402 @request.session[:user_id] = 2 # manager
411 @request.session[:user_id] = 2 # manager
403 get :settings, :id => 1
412 get :settings, :id => 1
404 assert_response :success
413 assert_response :success
405 assert_template 'settings'
414 assert_template 'settings'
406 end
415 end
407
416
408 def test_settings_of_subproject
417 def test_settings_of_subproject
409 @request.session[:user_id] = 2
418 @request.session[:user_id] = 2
410 get :settings, :id => 'private-child'
419 get :settings, :id => 'private-child'
411 assert_response :success
420 assert_response :success
412 assert_template 'settings'
421 assert_template 'settings'
413
422
414 assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]'
423 assert_select 'input[type=checkbox][name=?]', 'project[inherit_members]'
415 end
424 end
416
425
417 def test_settings_should_be_denied_for_member_on_closed_project
426 def test_settings_should_be_denied_for_member_on_closed_project
418 Project.find(1).close
427 Project.find(1).close
419 @request.session[:user_id] = 2 # manager
428 @request.session[:user_id] = 2 # manager
420
429
421 get :settings, :id => 1
430 get :settings, :id => 1
422 assert_response 403
431 assert_response 403
423 end
432 end
424
433
425 def test_settings_should_be_denied_for_anonymous_on_closed_project
434 def test_settings_should_be_denied_for_anonymous_on_closed_project
426 Project.find(1).close
435 Project.find(1).close
427
436
428 get :settings, :id => 1
437 get :settings, :id => 1
429 assert_response 302
438 assert_response 302
430 end
439 end
431
440
432 def test_setting_with_wiki_module_and_no_wiki
441 def test_setting_with_wiki_module_and_no_wiki
433 Project.find(1).wiki.destroy
442 Project.find(1).wiki.destroy
434 Role.find(1).add_permission! :manage_wiki
443 Role.find(1).add_permission! :manage_wiki
435 @request.session[:user_id] = 2
444 @request.session[:user_id] = 2
436
445
437 get :settings, :id => 1
446 get :settings, :id => 1
438 assert_response :success
447 assert_response :success
439 assert_template 'settings'
448 assert_template 'settings'
440
449
441 assert_select 'form[action=?]', '/projects/ecookbook/wiki' do
450 assert_select 'form[action=?]', '/projects/ecookbook/wiki' do
442 assert_select 'input[name=?]', 'wiki[start_page]'
451 assert_select 'input[name=?]', 'wiki[start_page]'
443 end
452 end
444 end
453 end
445
454
446 def test_update
455 def test_update
447 @request.session[:user_id] = 2 # manager
456 @request.session[:user_id] = 2 # manager
448 post :update, :id => 1, :project => {:name => 'Test changed name',
457 post :update, :id => 1, :project => {:name => 'Test changed name',
449 :issue_custom_field_ids => ['']}
458 :issue_custom_field_ids => ['']}
450 assert_redirected_to '/projects/ecookbook/settings'
459 assert_redirected_to '/projects/ecookbook/settings'
451 project = Project.find(1)
460 project = Project.find(1)
452 assert_equal 'Test changed name', project.name
461 assert_equal 'Test changed name', project.name
453 end
462 end
454
463
455 def test_update_with_failure
464 def test_update_with_failure
456 @request.session[:user_id] = 2 # manager
465 @request.session[:user_id] = 2 # manager
457 post :update, :id => 1, :project => {:name => ''}
466 post :update, :id => 1, :project => {:name => ''}
458 assert_response :success
467 assert_response :success
459 assert_template 'settings'
468 assert_template 'settings'
460 assert_select_error /name cannot be blank/i
469 assert_select_error /name cannot be blank/i
461 end
470 end
462
471
463 def test_update_should_be_denied_for_member_on_closed_project
472 def test_update_should_be_denied_for_member_on_closed_project
464 Project.find(1).close
473 Project.find(1).close
465 @request.session[:user_id] = 2 # manager
474 @request.session[:user_id] = 2 # manager
466
475
467 post :update, :id => 1, :project => {:name => 'Closed'}
476 post :update, :id => 1, :project => {:name => 'Closed'}
468 assert_response 403
477 assert_response 403
469 assert_equal 'eCookbook', Project.find(1).name
478 assert_equal 'eCookbook', Project.find(1).name
470 end
479 end
471
480
472 def test_update_should_be_denied_for_anonymous_on_closed_project
481 def test_update_should_be_denied_for_anonymous_on_closed_project
473 Project.find(1).close
482 Project.find(1).close
474
483
475 post :update, :id => 1, :project => {:name => 'Closed'}
484 post :update, :id => 1, :project => {:name => 'Closed'}
476 assert_response 302
485 assert_response 302
477 assert_equal 'eCookbook', Project.find(1).name
486 assert_equal 'eCookbook', Project.find(1).name
478 end
487 end
479
488
480 def test_modules
489 def test_modules
481 @request.session[:user_id] = 2
490 @request.session[:user_id] = 2
482 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
491 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
483
492
484 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
493 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
485 assert_redirected_to '/projects/ecookbook/settings/modules'
494 assert_redirected_to '/projects/ecookbook/settings/modules'
486 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
495 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
487 end
496 end
488
497
489 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
498 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
490 @request.session[:user_id] = 1 # admin
499 @request.session[:user_id] = 1 # admin
491
500
492 assert_no_difference 'Project.count' do
501 assert_no_difference 'Project.count' do
493 delete :destroy, :id => 2
502 delete :destroy, :id => 2
494 assert_response :success
503 assert_response :success
495 assert_template 'destroy'
504 assert_template 'destroy'
496 end
505 end
497 end
506 end
498
507
499 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
508 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
500 @request.session[:user_id] = 1 # admin
509 @request.session[:user_id] = 1 # admin
501
510
502 assert_no_difference 'Project.count' do
511 assert_no_difference 'Project.count' do
503 delete :destroy, :id => 1
512 delete :destroy, :id => 1
504 assert_response :success
513 assert_response :success
505 assert_template 'destroy'
514 assert_template 'destroy'
506 end
515 end
507 assert_select 'strong',
516 assert_select 'strong',
508 :text => ['Private child of eCookbook',
517 :text => ['Private child of eCookbook',
509 'Child of private child, eCookbook Subproject 1',
518 'Child of private child, eCookbook Subproject 1',
510 'eCookbook Subproject 2'].join(', ')
519 'eCookbook Subproject 2'].join(', ')
511 end
520 end
512
521
513 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
522 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
514 @request.session[:user_id] = 1 # admin
523 @request.session[:user_id] = 1 # admin
515
524
516 assert_difference 'Project.count', -5 do
525 assert_difference 'Project.count', -5 do
517 delete :destroy, :id => 1, :confirm => 1
526 delete :destroy, :id => 1, :confirm => 1
518 assert_redirected_to '/admin/projects'
527 assert_redirected_to '/admin/projects'
519 end
528 end
520 assert_nil Project.find_by_id(1)
529 assert_nil Project.find_by_id(1)
521 end
530 end
522
531
523 def test_archive
532 def test_archive
524 @request.session[:user_id] = 1 # admin
533 @request.session[:user_id] = 1 # admin
525 post :archive, :id => 1
534 post :archive, :id => 1
526 assert_redirected_to '/admin/projects'
535 assert_redirected_to '/admin/projects'
527 assert !Project.find(1).active?
536 assert !Project.find(1).active?
528 end
537 end
529
538
530 def test_archive_with_failure
539 def test_archive_with_failure
531 @request.session[:user_id] = 1
540 @request.session[:user_id] = 1
532 Project.any_instance.stubs(:archive).returns(false)
541 Project.any_instance.stubs(:archive).returns(false)
533 post :archive, :id => 1
542 post :archive, :id => 1
534 assert_redirected_to '/admin/projects'
543 assert_redirected_to '/admin/projects'
535 assert_match /project cannot be archived/i, flash[:error]
544 assert_match /project cannot be archived/i, flash[:error]
536 end
545 end
537
546
538 def test_unarchive
547 def test_unarchive
539 @request.session[:user_id] = 1 # admin
548 @request.session[:user_id] = 1 # admin
540 Project.find(1).archive
549 Project.find(1).archive
541 post :unarchive, :id => 1
550 post :unarchive, :id => 1
542 assert_redirected_to '/admin/projects'
551 assert_redirected_to '/admin/projects'
543 assert Project.find(1).active?
552 assert Project.find(1).active?
544 end
553 end
545
554
546 def test_close
555 def test_close
547 @request.session[:user_id] = 2
556 @request.session[:user_id] = 2
548 post :close, :id => 1
557 post :close, :id => 1
549 assert_redirected_to '/projects/ecookbook'
558 assert_redirected_to '/projects/ecookbook'
550 assert_equal Project::STATUS_CLOSED, Project.find(1).status
559 assert_equal Project::STATUS_CLOSED, Project.find(1).status
551 end
560 end
552
561
553 def test_reopen
562 def test_reopen
554 Project.find(1).close
563 Project.find(1).close
555 @request.session[:user_id] = 2
564 @request.session[:user_id] = 2
556 post :reopen, :id => 1
565 post :reopen, :id => 1
557 assert_redirected_to '/projects/ecookbook'
566 assert_redirected_to '/projects/ecookbook'
558 assert Project.find(1).active?
567 assert Project.find(1).active?
559 end
568 end
560
569
561 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
570 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
562 CustomField.delete_all
571 CustomField.delete_all
563 parent = nil
572 parent = nil
564 6.times do |i|
573 6.times do |i|
565 p = Project.generate_with_parent!(parent)
574 p = Project.generate_with_parent!(parent)
566 get :show, :id => p
575 get :show, :id => p
567 assert_select '#header h1' do
576 assert_select '#header h1' do
568 assert_select 'a', :count => [i, 3].min
577 assert_select 'a', :count => [i, 3].min
569 end
578 end
570
579
571 parent = p
580 parent = p
572 end
581 end
573 end
582 end
574
583
575 def test_get_copy
584 def test_get_copy
576 @request.session[:user_id] = 1 # admin
585 @request.session[:user_id] = 1 # admin
577 get :copy, :id => 1
586 get :copy, :id => 1
578 assert_response :success
587 assert_response :success
579 assert_template 'copy'
588 assert_template 'copy'
580 assert assigns(:project)
589 assert assigns(:project)
581 assert_equal Project.find(1).description, assigns(:project).description
590 assert_equal Project.find(1).description, assigns(:project).description
582 assert_nil assigns(:project).id
591 assert_nil assigns(:project).id
583
592
584 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
593 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
585 end
594 end
586
595
587 def test_get_copy_with_invalid_source_should_respond_with_404
596 def test_get_copy_with_invalid_source_should_respond_with_404
588 @request.session[:user_id] = 1
597 @request.session[:user_id] = 1
589 get :copy, :id => 99
598 get :copy, :id => 99
590 assert_response 404
599 assert_response 404
591 end
600 end
592
601
593 def test_post_copy_should_copy_requested_items
602 def test_post_copy_should_copy_requested_items
594 @request.session[:user_id] = 1 # admin
603 @request.session[:user_id] = 1 # admin
595 CustomField.delete_all
604 CustomField.delete_all
596
605
597 assert_difference 'Project.count' do
606 assert_difference 'Project.count' do
598 post :copy, :id => 1,
607 post :copy, :id => 1,
599 :project => {
608 :project => {
600 :name => 'Copy',
609 :name => 'Copy',
601 :identifier => 'unique-copy',
610 :identifier => 'unique-copy',
602 :tracker_ids => ['1', '2', '3', ''],
611 :tracker_ids => ['1', '2', '3', ''],
603 :enabled_module_names => %w(issue_tracking time_tracking)
612 :enabled_module_names => %w(issue_tracking time_tracking)
604 },
613 },
605 :only => %w(issues versions)
614 :only => %w(issues versions)
606 end
615 end
607 project = Project.find('unique-copy')
616 project = Project.find('unique-copy')
608 source = Project.find(1)
617 source = Project.find(1)
609 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
618 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
610
619
611 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
620 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
612 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
621 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
613 assert_equal 0, project.members.count
622 assert_equal 0, project.members.count
614 end
623 end
615
624
616 def test_post_copy_should_redirect_to_settings_when_successful
625 def test_post_copy_should_redirect_to_settings_when_successful
617 @request.session[:user_id] = 1 # admin
626 @request.session[:user_id] = 1 # admin
618 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
627 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
619 assert_response :redirect
628 assert_response :redirect
620 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
629 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
621 end
630 end
622
631
623 def test_post_copy_with_failure
632 def test_post_copy_with_failure
624 @request.session[:user_id] = 1
633 @request.session[:user_id] = 1
625 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => ''}
634 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => ''}
626 assert_response :success
635 assert_response :success
627 assert_template 'copy'
636 assert_template 'copy'
628 end
637 end
629
638
630 def test_jump_should_redirect_to_active_tab
639 def test_jump_should_redirect_to_active_tab
631 get :show, :id => 1, :jump => 'issues'
640 get :show, :id => 1, :jump => 'issues'
632 assert_redirected_to '/projects/ecookbook/issues'
641 assert_redirected_to '/projects/ecookbook/issues'
633 end
642 end
634
643
635 def test_jump_should_not_redirect_to_inactive_tab
644 def test_jump_should_not_redirect_to_inactive_tab
636 get :show, :id => 3, :jump => 'documents'
645 get :show, :id => 3, :jump => 'documents'
637 assert_response :success
646 assert_response :success
638 assert_template 'show'
647 assert_template 'show'
639 end
648 end
640
649
641 def test_jump_should_not_redirect_to_unknown_tab
650 def test_jump_should_not_redirect_to_unknown_tab
642 get :show, :id => 3, :jump => 'foobar'
651 get :show, :id => 3, :jump => 'foobar'
643 assert_response :success
652 assert_response :success
644 assert_template 'show'
653 assert_template 'show'
645 end
654 end
646
655
647 def test_body_should_have_project_css_class
656 def test_body_should_have_project_css_class
648 get :show, :id => 1
657 get :show, :id => 1
649 assert_select 'body.project-ecookbook'
658 assert_select 'body.project-ecookbook'
650 end
659 end
651
660
652 def test_project_menu_should_include_new_issue_link
661 def test_project_menu_should_include_new_issue_link
653 @request.session[:user_id] = 2
662 @request.session[:user_id] = 2
654 get :show, :id => 1
663 get :show, :id => 1
655 assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
664 assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
656 end
665 end
657
666
658 def test_project_menu_should_not_include_new_issue_link_for_project_without_trackers
667 def test_project_menu_should_not_include_new_issue_link_for_project_without_trackers
659 Project.find(1).trackers.clear
668 Project.find(1).trackers.clear
660
669
661 @request.session[:user_id] = 2
670 @request.session[:user_id] = 2
662 get :show, :id => 1
671 get :show, :id => 1
663 assert_select '#main-menu a.new-issue', 0
672 assert_select '#main-menu a.new-issue', 0
664 end
673 end
665 end
674 end
General Comments 0
You need to be logged in to leave comments. Login now