##// END OF EJS Templates
Fixed: unable to clear value for list custom field with multiple values and checkboxes style (#16798)....
Jean-Philippe Lang -
r12892:cf4af2c5db0d
parent child
Show More
@@ -1,685 +1,688
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 def self.add(name)
74 74 self.format_name = name
75 75 Redmine::FieldFormat.add(name, self)
76 76 end
77 77 private_class_method :add
78 78
79 79 def self.field_attributes(*args)
80 80 CustomField.store_accessor :format_store, *args
81 81 end
82 82
83 83 field_attributes :url_pattern
84 84
85 85 def name
86 86 self.class.format_name
87 87 end
88 88
89 89 def label
90 90 "label_#{name}"
91 91 end
92 92
93 93 def cast_custom_value(custom_value)
94 94 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
95 95 end
96 96
97 97 def cast_value(custom_field, value, customized=nil)
98 98 if value.blank?
99 99 nil
100 100 elsif value.is_a?(Array)
101 101 casted = value.map do |v|
102 102 cast_single_value(custom_field, v, customized)
103 103 end
104 104 casted.compact.sort
105 105 else
106 106 cast_single_value(custom_field, value, customized)
107 107 end
108 108 end
109 109
110 110 def cast_single_value(custom_field, value, customized=nil)
111 111 value.to_s
112 112 end
113 113
114 114 def target_class
115 115 nil
116 116 end
117 117
118 118 def possible_custom_value_options(custom_value)
119 119 possible_values_options(custom_value.custom_field, custom_value.customized)
120 120 end
121 121
122 122 def possible_values_options(custom_field, object=nil)
123 123 []
124 124 end
125 125
126 126 # Returns the validation errors for custom_field
127 127 # Should return an empty array if custom_field is valid
128 128 def validate_custom_field(custom_field)
129 129 []
130 130 end
131 131
132 132 # Returns the validation error messages for custom_value
133 133 # Should return an empty array if custom_value is valid
134 134 def validate_custom_value(custom_value)
135 135 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
136 136 errors = values.map do |value|
137 137 validate_single_value(custom_value.custom_field, value, custom_value.customized)
138 138 end
139 139 errors.flatten.uniq
140 140 end
141 141
142 142 def validate_single_value(custom_field, value, customized=nil)
143 143 []
144 144 end
145 145
146 146 def formatted_custom_value(view, custom_value, html=false)
147 147 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
148 148 end
149 149
150 150 def formatted_value(view, custom_field, value, customized=nil, html=false)
151 151 casted = cast_value(custom_field, value, customized)
152 152 if html && custom_field.url_pattern.present?
153 153 texts_and_urls = Array.wrap(casted).map do |single_value|
154 154 text = view.format_object(single_value, false).to_s
155 155 url = url_from_pattern(custom_field, single_value, customized)
156 156 [text, url]
157 157 end
158 158 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
159 159 links.join(', ').html_safe
160 160 else
161 161 casted
162 162 end
163 163 end
164 164
165 165 # Returns an URL generated with the custom field URL pattern
166 166 # and variables substitution:
167 167 # %value% => the custom field value
168 168 # %id% => id of the customized object
169 169 # %project_id% => id of the project of the customized object if defined
170 170 # %project_identifier% => identifier of the project of the customized object if defined
171 171 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
172 172 def url_from_pattern(custom_field, value, customized)
173 173 url = custom_field.url_pattern.to_s.dup
174 174 url.gsub!('%value%') {value.to_s}
175 175 url.gsub!('%id%') {customized.id.to_s}
176 176 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
177 177 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
178 178 if custom_field.regexp.present?
179 179 url.gsub!(%r{%m(\d+)%}) do
180 180 m = $1.to_i
181 181 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
182 182 matches[m].to_s
183 183 end
184 184 end
185 185 end
186 186 url
187 187 end
188 188 protected :url_from_pattern
189 189
190 190 def edit_tag(view, tag_id, tag_name, custom_value, options={})
191 191 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
192 192 end
193 193
194 194 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
195 195 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
196 196 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
197 197 end
198 198
199 199 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
200 200 if custom_field.is_required?
201 201 ''.html_safe
202 202 else
203 203 view.content_tag('label',
204 204 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
205 205 :class => 'inline'
206 206 )
207 207 end
208 208 end
209 209 protected :bulk_clear_tag
210 210
211 211 def query_filter_options(custom_field, query)
212 212 {:type => :string}
213 213 end
214 214
215 215 def before_custom_field_save(custom_field)
216 216 end
217 217
218 218 # Returns a ORDER BY clause that can used to sort customized
219 219 # objects by their value of the custom field.
220 220 # Returns nil if the custom field can not be used for sorting.
221 221 def order_statement(custom_field)
222 222 # COALESCE is here to make sure that blank and NULL values are sorted equally
223 223 "COALESCE(#{join_alias custom_field}.value, '')"
224 224 end
225 225
226 226 # Returns a GROUP BY clause that can used to group by custom value
227 227 # Returns nil if the custom field can not be used for grouping.
228 228 def group_statement(custom_field)
229 229 nil
230 230 end
231 231
232 232 # Returns a JOIN clause that is added to the query when sorting by custom values
233 233 def join_for_order_statement(custom_field)
234 234 alias_name = join_alias(custom_field)
235 235
236 236 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
237 237 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
238 238 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
239 239 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
240 240 " AND (#{custom_field.visibility_by_project_condition})" +
241 241 " AND #{alias_name}.value <> ''" +
242 242 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
243 243 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
244 244 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
245 245 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
246 246 end
247 247
248 248 def join_alias(custom_field)
249 249 "cf_#{custom_field.id}"
250 250 end
251 251 protected :join_alias
252 252 end
253 253
254 254 class Unbounded < Base
255 255 def validate_single_value(custom_field, value, customized=nil)
256 256 errs = super
257 257 value = value.to_s
258 258 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
259 259 errs << ::I18n.t('activerecord.errors.messages.invalid')
260 260 end
261 261 if custom_field.min_length && value.length < custom_field.min_length
262 262 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
263 263 end
264 264 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
265 265 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
266 266 end
267 267 errs
268 268 end
269 269 end
270 270
271 271 class StringFormat < Unbounded
272 272 add 'string'
273 273 self.searchable_supported = true
274 274 self.form_partial = 'custom_fields/formats/string'
275 275 field_attributes :text_formatting
276 276
277 277 def formatted_value(view, custom_field, value, customized=nil, html=false)
278 278 if html
279 279 if custom_field.url_pattern.present?
280 280 super
281 281 elsif custom_field.text_formatting == 'full'
282 282 view.textilizable(value, :object => customized)
283 283 else
284 284 value.to_s
285 285 end
286 286 else
287 287 value.to_s
288 288 end
289 289 end
290 290 end
291 291
292 292 class TextFormat < Unbounded
293 293 add 'text'
294 294 self.searchable_supported = true
295 295 self.form_partial = 'custom_fields/formats/text'
296 296
297 297 def formatted_value(view, custom_field, value, customized=nil, html=false)
298 298 if html
299 299 if custom_field.text_formatting == 'full'
300 300 view.textilizable(value, :object => customized)
301 301 else
302 302 view.simple_format(html_escape(value))
303 303 end
304 304 else
305 305 value.to_s
306 306 end
307 307 end
308 308
309 309 def edit_tag(view, tag_id, tag_name, custom_value, options={})
310 310 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
311 311 end
312 312
313 313 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
314 314 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
315 315 '<br />'.html_safe +
316 316 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
317 317 end
318 318
319 319 def query_filter_options(custom_field, query)
320 320 {:type => :text}
321 321 end
322 322 end
323 323
324 324 class LinkFormat < StringFormat
325 325 add 'link'
326 326 self.searchable_supported = false
327 327 self.form_partial = 'custom_fields/formats/link'
328 328
329 329 def formatted_value(view, custom_field, value, customized=nil, html=false)
330 330 if html
331 331 if custom_field.url_pattern.present?
332 332 url = url_from_pattern(custom_field, value, customized)
333 333 else
334 334 url = value.to_s
335 335 unless url =~ %r{\A[a-z]+://}i
336 336 # no protocol found, use http by default
337 337 url = "http://" + url
338 338 end
339 339 end
340 340 view.link_to value.to_s, url
341 341 else
342 342 value.to_s
343 343 end
344 344 end
345 345 end
346 346
347 347 class Numeric < Unbounded
348 348 self.form_partial = 'custom_fields/formats/numeric'
349 349
350 350 def order_statement(custom_field)
351 351 # Make the database cast values into numeric
352 352 # Postgresql will raise an error if a value can not be casted!
353 353 # CustomValue validations should ensure that it doesn't occur
354 354 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
355 355 end
356 356 end
357 357
358 358 class IntFormat < Numeric
359 359 add 'int'
360 360
361 361 def label
362 362 "label_integer"
363 363 end
364 364
365 365 def cast_single_value(custom_field, value, customized=nil)
366 366 value.to_i
367 367 end
368 368
369 369 def validate_single_value(custom_field, value, customized=nil)
370 370 errs = super
371 371 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
372 372 errs
373 373 end
374 374
375 375 def query_filter_options(custom_field, query)
376 376 {:type => :integer}
377 377 end
378 378
379 379 def group_statement(custom_field)
380 380 order_statement(custom_field)
381 381 end
382 382 end
383 383
384 384 class FloatFormat < Numeric
385 385 add 'float'
386 386
387 387 def cast_single_value(custom_field, value, customized=nil)
388 388 value.to_f
389 389 end
390 390
391 391 def validate_single_value(custom_field, value, customized=nil)
392 392 errs = super
393 393 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
394 394 errs
395 395 end
396 396
397 397 def query_filter_options(custom_field, query)
398 398 {:type => :float}
399 399 end
400 400 end
401 401
402 402 class DateFormat < Unbounded
403 403 add 'date'
404 404 self.form_partial = 'custom_fields/formats/date'
405 405
406 406 def cast_single_value(custom_field, value, customized=nil)
407 407 value.to_date rescue nil
408 408 end
409 409
410 410 def validate_single_value(custom_field, value, customized=nil)
411 411 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
412 412 []
413 413 else
414 414 [::I18n.t('activerecord.errors.messages.not_a_date')]
415 415 end
416 416 end
417 417
418 418 def edit_tag(view, tag_id, tag_name, custom_value, options={})
419 419 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
420 420 view.calendar_for(tag_id)
421 421 end
422 422
423 423 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
424 424 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
425 425 view.calendar_for(tag_id) +
426 426 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
427 427 end
428 428
429 429 def query_filter_options(custom_field, query)
430 430 {:type => :date}
431 431 end
432 432
433 433 def group_statement(custom_field)
434 434 order_statement(custom_field)
435 435 end
436 436 end
437 437
438 438 class List < Base
439 439 self.multiple_supported = true
440 440 field_attributes :edit_tag_style
441 441
442 442 def edit_tag(view, tag_id, tag_name, custom_value, options={})
443 443 if custom_value.custom_field.edit_tag_style == 'check_box'
444 444 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
445 445 else
446 446 select_edit_tag(view, tag_id, tag_name, custom_value, options)
447 447 end
448 448 end
449 449
450 450 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
451 451 opts = []
452 452 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
453 453 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
454 454 opts += possible_values_options(custom_field, objects)
455 455 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
456 456 end
457 457
458 458 def query_filter_options(custom_field, query)
459 459 {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
460 460 end
461 461
462 462 protected
463 463
464 464 # Renders the edit tag as a select tag
465 465 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
466 466 blank_option = ''.html_safe
467 467 unless custom_value.custom_field.multiple?
468 468 if custom_value.custom_field.is_required?
469 469 unless custom_value.custom_field.default_value.present?
470 470 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
471 471 end
472 472 else
473 473 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
474 474 end
475 475 end
476 476 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
477 477 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
478 478 if custom_value.custom_field.multiple?
479 479 s << view.hidden_field_tag(tag_name, '')
480 480 end
481 481 s
482 482 end
483 483
484 484 # Renders the edit tag as check box or radio tags
485 485 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
486 486 opts = []
487 487 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
488 488 opts << ["(#{l(:label_none)})", '']
489 489 end
490 490 opts += possible_custom_value_options(custom_value)
491 491 s = ''.html_safe
492 492 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
493 493 opts.each do |label, value|
494 494 value ||= label
495 495 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
496 496 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
497 497 # set the id on the first tag only
498 498 tag_id = nil
499 499 s << view.content_tag('label', tag + ' ' + label)
500 500 end
501 if custom_value.custom_field.multiple?
502 s << view.hidden_field_tag(tag_name, '')
503 end
501 504 css = "#{options[:class]} check_box_group"
502 505 view.content_tag('span', s, options.merge(:class => css))
503 506 end
504 507 end
505 508
506 509 class ListFormat < List
507 510 add 'list'
508 511 self.searchable_supported = true
509 512 self.form_partial = 'custom_fields/formats/list'
510 513
511 514 def possible_custom_value_options(custom_value)
512 515 options = possible_values_options(custom_value.custom_field)
513 516 missing = [custom_value.value].flatten.reject(&:blank?) - options
514 517 if missing.any?
515 518 options += missing
516 519 end
517 520 options
518 521 end
519 522
520 523 def possible_values_options(custom_field, object=nil)
521 524 custom_field.possible_values
522 525 end
523 526
524 527 def validate_custom_field(custom_field)
525 528 errors = []
526 529 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
527 530 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
528 531 errors
529 532 end
530 533
531 534 def validate_custom_value(custom_value)
532 535 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
533 536 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
534 537 if invalid_values.any?
535 538 [::I18n.t('activerecord.errors.messages.inclusion')]
536 539 else
537 540 []
538 541 end
539 542 end
540 543
541 544 def group_statement(custom_field)
542 545 order_statement(custom_field)
543 546 end
544 547 end
545 548
546 549 class BoolFormat < List
547 550 add 'bool'
548 551 self.multiple_supported = false
549 552 self.form_partial = 'custom_fields/formats/bool'
550 553
551 554 def label
552 555 "label_boolean"
553 556 end
554 557
555 558 def cast_single_value(custom_field, value, customized=nil)
556 559 value == '1' ? true : false
557 560 end
558 561
559 562 def possible_values_options(custom_field, object=nil)
560 563 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
561 564 end
562 565
563 566 def group_statement(custom_field)
564 567 order_statement(custom_field)
565 568 end
566 569 end
567 570
568 571 class RecordList < List
569 572 self.customized_class_names = %w(Issue TimeEntry Version Project)
570 573
571 574 def cast_single_value(custom_field, value, customized=nil)
572 575 target_class.find_by_id(value.to_i) if value.present?
573 576 end
574 577
575 578 def target_class
576 579 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
577 580 end
578 581
579 582 def possible_custom_value_options(custom_value)
580 583 options = possible_values_options(custom_value.custom_field, custom_value.customized)
581 584 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
582 585 if missing.any?
583 586 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
584 587 #TODO: use #sort_by! when ruby1.8 support is dropped
585 588 options = options.sort_by(&:first)
586 589 end
587 590 options
588 591 end
589 592
590 593 def order_statement(custom_field)
591 594 if target_class.respond_to?(:fields_for_order_statement)
592 595 target_class.fields_for_order_statement(value_join_alias(custom_field))
593 596 end
594 597 end
595 598
596 599 def group_statement(custom_field)
597 600 "COALESCE(#{join_alias custom_field}.value, '')"
598 601 end
599 602
600 603 def join_for_order_statement(custom_field)
601 604 alias_name = join_alias(custom_field)
602 605
603 606 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
604 607 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
605 608 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
606 609 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
607 610 " AND (#{custom_field.visibility_by_project_condition})" +
608 611 " AND #{alias_name}.value <> ''" +
609 612 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
610 613 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
611 614 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
612 615 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
613 616 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
614 617 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
615 618 end
616 619
617 620 def value_join_alias(custom_field)
618 621 join_alias(custom_field) + "_" + custom_field.field_format
619 622 end
620 623 protected :value_join_alias
621 624 end
622 625
623 626 class UserFormat < RecordList
624 627 add 'user'
625 628 self.form_partial = 'custom_fields/formats/user'
626 629 field_attributes :user_role
627 630
628 631 def possible_values_options(custom_field, object=nil)
629 632 if object.is_a?(Array)
630 633 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
631 634 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
632 635 elsif object.respond_to?(:project) && object.project
633 636 scope = object.project.users
634 637 if custom_field.user_role.is_a?(Array)
635 638 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
636 639 if role_ids.any?
637 640 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
638 641 end
639 642 end
640 643 scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
641 644 else
642 645 []
643 646 end
644 647 end
645 648
646 649 def before_custom_field_save(custom_field)
647 650 super
648 651 if custom_field.user_role.is_a?(Array)
649 652 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
650 653 end
651 654 end
652 655 end
653 656
654 657 class VersionFormat < RecordList
655 658 add 'version'
656 659 self.form_partial = 'custom_fields/formats/version'
657 660 field_attributes :version_status
658 661
659 662 def possible_values_options(custom_field, object=nil)
660 663 if object.is_a?(Array)
661 664 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
662 665 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
663 666 elsif object.respond_to?(:project) && object.project
664 667 scope = object.project.shared_versions
665 668 if custom_field.version_status.is_a?(Array)
666 669 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
667 670 if statuses.any?
668 671 scope = scope.where(:status => statuses.map(&:to_s))
669 672 end
670 673 end
671 674 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
672 675 else
673 676 []
674 677 end
675 678 end
676 679
677 680 def before_custom_field_save(custom_field)
678 681 super
679 682 if custom_field.version_status.is_a?(Array)
680 683 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
681 684 end
682 685 end
683 686 end
684 687 end
685 688 end
@@ -1,156 +1,168
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 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 require 'redmine/field_format'
20 20
21 21 class Redmine::ListFieldFormatTest < ActionView::TestCase
22 22 include ApplicationHelper
23 23 include Redmine::I18n
24 24
25 25 def setup
26 26 set_language_if_valid 'en'
27 27 end
28 28
29 29 def test_possible_existing_value_should_be_valid
30 30 field = GroupCustomField.create!(:name => 'List', :field_format => 'list', :possible_values => ['Foo', 'Bar'])
31 31 group = Group.new(:name => 'Group')
32 32 group.custom_field_values = {field.id => 'Baz'}
33 33 assert group.save(:validate => false)
34 34
35 35 group = Group.order('id DESC').first
36 36 assert_equal ['Foo', 'Bar', 'Baz'], field.possible_custom_value_options(group.custom_value_for(field))
37 37 assert group.valid?
38 38 end
39 39
40 40 def test_edit_tag_should_have_id_and_name
41 41 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false)
42 42 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new)
43 43
44 44 tag = field.format.edit_tag(self, 'abc', 'xyz', value)
45 45 assert_select_in tag, 'select[id=abc][name=xyz]'
46 46 end
47 47
48 48 def test_edit_tag_should_contain_possible_values
49 49 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false)
50 50 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new)
51 51
52 52 tag = field.format.edit_tag(self, 'id', 'name', value)
53 53 assert_select_in tag, 'select' do
54 54 assert_select 'option', 3
55 55 assert_select 'option[value=]'
56 56 assert_select 'option[value=Foo]', :text => 'Foo'
57 57 assert_select 'option[value=Bar]', :text => 'Bar'
58 58 end
59 59 end
60 60
61 61 def test_edit_tag_should_select_current_value
62 62 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false)
63 63 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => 'Bar')
64 64
65 65 tag = field.format.edit_tag(self, 'id', 'name', value)
66 66 assert_select_in tag, 'select' do
67 67 assert_select 'option[selected=selected]', 1
68 68 assert_select 'option[value=Bar][selected=selected]', :text => 'Bar'
69 69 end
70 70 end
71 71
72 72 def test_edit_tag_with_multiple_should_select_current_values
73 73 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar', 'Baz'], :is_required => false,
74 74 :multiple => true)
75 75 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => ['Bar', 'Baz'])
76 76
77 77 tag = field.format.edit_tag(self, 'id', 'name', value)
78 78 assert_select_in tag, 'select[multiple=multiple]' do
79 79 assert_select 'option[selected=selected]', 2
80 80 assert_select 'option[value=Bar][selected=selected]', :text => 'Bar'
81 81 assert_select 'option[value=Baz][selected=selected]', :text => 'Baz'
82 82 end
83 83 end
84 84
85 85 def test_edit_tag_with_check_box_style_should_contain_possible_values
86 86 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false,
87 87 :edit_tag_style => 'check_box')
88 88 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new)
89 89
90 90 tag = field.format.edit_tag(self, 'id', 'name', value)
91 91 assert_select_in tag, 'span' do
92 92 assert_select 'input[type=radio]', 3
93 93 assert_select 'label', :text => '(none)' do
94 94 assert_select 'input[value=]'
95 95 end
96 96 assert_select 'label', :text => 'Foo' do
97 97 assert_select 'input[value=Foo]'
98 98 end
99 99 assert_select 'label', :text => 'Bar' do
100 100 assert_select 'input[value=Bar]'
101 101 end
102 102 end
103 103 end
104 104
105 105 def test_edit_tag_with_check_box_style_should_select_current_value
106 106 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false,
107 107 :edit_tag_style => 'check_box')
108 108 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => 'Bar')
109 109
110 110 tag = field.format.edit_tag(self, 'id', 'name', value)
111 111 assert_select_in tag, 'span' do
112 112 assert_select 'input[type=radio][checked=checked]', 1
113 113 assert_select 'label', :text => 'Bar' do
114 114 assert_select 'input[value=Bar][checked=checked]'
115 115 end
116 116 end
117 117 end
118 118
119 def test_edit_tag_with_check_box_style_and_multiple_values_should_contain_hidden_field_to_clear_value
120 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar'], :is_required => false,
121 :edit_tag_style => 'check_box', :multiple => true)
122 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new)
123
124 tag = field.format.edit_tag(self, 'id', 'name', value)
125 assert_select_in tag, 'span' do
126 assert_select 'input[type=checkbox]', 2
127 assert_select 'input[type=hidden]', 1
128 end
129 end
130
119 131 def test_field_with_url_pattern_should_link_value
120 132 field = IssueCustomField.new(:field_format => 'list', :url_pattern => 'http://localhost/%value%')
121 133 formatted = field.format.formatted_value(self, field, 'foo', Issue.new, true)
122 134 assert_equal '<a href="http://localhost/foo">foo</a>', formatted
123 135 assert formatted.html_safe?
124 136 end
125 137
126 138 def test_field_with_url_pattern_and_multiple_values_should_link_values
127 139 field = IssueCustomField.new(:field_format => 'list', :url_pattern => 'http://localhost/%value%')
128 140 formatted = field.format.formatted_value(self, field, ['foo', 'bar'], Issue.new, true)
129 141 assert_equal '<a href="http://localhost/bar">bar</a>, <a href="http://localhost/foo">foo</a>', formatted
130 142 assert formatted.html_safe?
131 143 end
132 144
133 145 def test_field_with_url_pattern_should_not_link_blank_value
134 146 field = IssueCustomField.new(:field_format => 'list', :url_pattern => 'http://localhost/%value%')
135 147 formatted = field.format.formatted_value(self, field, '', Issue.new, true)
136 148 assert_equal '', formatted
137 149 assert formatted.html_safe?
138 150 end
139 151
140 152 def test_edit_tag_with_check_box_style_and_multiple_should_select_current_values
141 153 field = IssueCustomField.new(:field_format => 'list', :possible_values => ['Foo', 'Bar', 'Baz'], :is_required => false,
142 154 :multiple => true, :edit_tag_style => 'check_box')
143 155 value = CustomFieldValue.new(:custom_field => field, :customized => Issue.new, :value => ['Bar', 'Baz'])
144 156
145 157 tag = field.format.edit_tag(self, 'id', 'name', value)
146 158 assert_select_in tag, 'span' do
147 159 assert_select 'input[type=checkbox][checked=checked]', 2
148 160 assert_select 'label', :text => 'Bar' do
149 161 assert_select 'input[value=Bar][checked=checked]'
150 162 end
151 163 assert_select 'label', :text => 'Baz' do
152 164 assert_select 'input[value=Baz][checked=checked]'
153 165 end
154 166 end
155 167 end
156 168 end
General Comments 0
You need to be logged in to leave comments. Login now