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