annotate lib/redmine/field_format.rb @ 1561:6074fffd8a1d feature_1136

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