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