custom_field.rb 7.49 KB
Newer Older
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3 4 5 6 7
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
8
#
9 10 11 12
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
13
#
14 15 16 17 18
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

class CustomField < ActiveRecord::Base
19 20
  include Redmine::SubclassFactory

21
  has_many :custom_values, :dependent => :delete_all
22
  acts_as_list :scope => 'type = \'#{self.class}\''
23
  serialize :possible_values
24

25
  validates_presence_of :name, :field_format
26
  validates_uniqueness_of :name, :scope => :type
27
  validates_length_of :name, :maximum => 30
28
  validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29

30
  validate :validate_custom_field
31
  before_validation :set_searchable
32

33
  def initialize(attributes=nil, *args)
34 35
    super
    self.possible_values ||= []
jplang's avatar
jplang committed
36
  end
37

38
  def set_searchable
39 40
    # make sure these fields are not searchable
    self.searchable = false if %w(int float date bool).include?(field_format)
41 42
    # make sure only these fields can have multiple values
    self.multiple = false unless %w(list user version).include?(field_format)
43
    true
44
  end
45

46
  def validate_custom_field
47
    if self.field_format == "list"
48 49
      errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
      errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
50
    end
51

52 53 54 55 56 57 58
    if regexp.present?
      begin
        Regexp.new(regexp)
      rescue
        errors.add(:regexp, :invalid)
      end
    end
59

60
    if default_value.present? && !valid_field_value?(default_value)
61 62
      errors.add(:default_value, :invalid)
    end
63
  end
64

65 66 67
  def possible_values_options(obj=nil)
    case field_format
    when 'user', 'version'
68
      if obj.respond_to?(:project) && obj.project
69 70 71 72
        case field_format
        when 'user'
          obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
        when 'version'
73
          obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
74
        end
75
      elsif obj.is_a?(Array)
76
        obj.collect {|o| possible_values_options(o)}.reduce(:&)
77 78 79
      else
        []
      end
80 81
    when 'bool'
      [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
82
    else
jplang's avatar
jplang committed
83
      possible_values || []
84 85
    end
  end
86

87 88
  def possible_values(obj=nil)
    case field_format
89
    when 'user', 'version'
90
      possible_values_options(obj).collect(&:last)
91 92
    when 'bool'
      ['1', '0']
93
    else
jplang's avatar
jplang committed
94 95 96 97 98 99 100
      values = super()
      if values.is_a?(Array)
        values.each do |value|
          value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
        end
      end
      values
101 102
    end
  end
103

104 105 106
  # Makes possible_values accept a multiline string
  def possible_values=(arg)
    if arg.is_a?(Array)
jplang's avatar
jplang committed
107
      super(arg.compact.collect(&:strip).select {|v| !v.blank?})
108 109 110 111
    else
      self.possible_values = arg.to_s.split(/[\n\r]+/)
    end
  end
112

113 114 115 116 117 118 119 120 121 122 123 124 125 126
  def cast_value(value)
    casted = nil
    unless value.blank?
      case field_format
      when 'string', 'text', 'list'
        casted = value
      when 'date'
        casted = begin; value.to_date; rescue; nil end
      when 'bool'
        casted = (value == '1' ? true : false)
      when 'int'
        casted = value.to_i
      when 'float'
        casted = value.to_f
127 128
      when 'user', 'version'
        casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
129 130 131 132
      end
    end
    casted
  end
133

134 135 136 137
  # Returns a ORDER BY clause that can used to sort customized
  # objects by their value of the custom field.
  # Returns false, if the custom field can not be used for sorting.
  def order_statement
138
    return nil if multiple?
139
    case field_format
140
      when 'string', 'text', 'list', 'date', 'bool'
141
        # COALESCE is here to make sure that blank and NULL values are sorted equally
142
        "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
143 144 145
          " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
          " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
146 147 148 149
      when 'int', 'float'
        # Make the database cast values into numeric
        # Postgresql will raise an error if a value can not be casted!
        # CustomValue validations should ensure that it doesn't occur
150
        "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
151 152 153
          " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
          " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
          " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
154 155 156 157
      else
        nil
    end
  end
158

159 160 161
  def <=>(field)
    position <=> field.position
  end
162

163 164 165 166
  def self.customized_class
    self.name =~ /^(.+)CustomField$/
    begin; $1.constantize; rescue nil; end
  end
167

168 169
  # to move in project_custom_field
  def self.for_all
170
    find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
171
  end
172

173 174 175
  def type_name
    nil
  end
176

177
  # Returns the error messages for the given value
178 179 180
  # or an empty array if value is a valid value for the custom field
  def validate_field_value(value)
    errs = []
181 182 183 184 185 186 187 188 189 190 191 192 193
    if value.is_a?(Array)
      if !multiple?
        errs << ::I18n.t('activerecord.errors.messages.invalid')
      end
      if is_required? && value.detect(&:present?).nil?
        errs << ::I18n.t('activerecord.errors.messages.blank')
      end
      value.each {|v| errs += validate_field_value_format(v)}
    else
      if is_required? && value.blank?
        errs << ::I18n.t('activerecord.errors.messages.blank')
      end
      errs += validate_field_value_format(value)
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
    end
    errs
  end

  # Returns true if value is a valid value for the custom field
  def valid_field_value?(value)
    validate_field_value(value).empty?
  end

  protected

  # Returns the error message for the given value regarding its format
  def validate_field_value_format(value)
    errs = []
    if value.present?
      errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
      errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
      errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length

      # Format specific validations
      case field_format
      when 'int'
        errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
      when 'float'
        begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
      when 'date'
        errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
      when 'list'
        errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
      end
    end
    errs
  end
227
end