File: app/models/custom_field.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: CustomField#18
includes
  SubclassFactory ( Redmine )
inherits from
  Base ( ActiveRecord )
has properties
method: initialize / 2 #33
method: set_searchable #38
method: validate_custom_field #46
method: possible_values_options / 1 #65
method: possible_values / 1 #87
method: possible_values= / 1 #99
method: cast_value / 1 #107
method: order_statement #131
method: <=> / 1 #153
class method: customized_class #157
class method: for_all #163
method: type_name #167
method: validate_field_value / 1 #173
method: valid_field_value? / 1 #193
method: validate_field_value_format / 1 #200
method: read_possible_values_utf8_encoded #222

Class Hierarchy

Object ( Builtin-Module )
Base ( ActiveRecord )
  CustomField    #18

Code

   1  # Redmine - project management software
   2  # Copyright (C) 2006-2012  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  class CustomField < ActiveRecord::Base
  19    include Redmine::SubclassFactory
  20 
  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      super
  35      self.possible_values ||= []
  36    end
  37 
  38    def set_searchable
  39      # make sure these fields are not searchable
  40      self.searchable = false if %w(int float date bool).include?(field_format)
  41      # make sure only these fields can have multiple values
  42      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        errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
  49        errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
  50      end
  51 
  52      if regexp.present?
  53        begin
  54          Regexp.new(regexp)
  55        rescue
  56          errors.add(:regexp, :invalid)
  57        end
  58      end
  59 
  60      if default_value.present? && !valid_field_value?(default_value)
  61        errors.add(:default_value, :invalid)
  62      end
  63    end
  64 
  65    def possible_values_options(obj=nil)
  66      case field_format
  67      when 'user', 'version'
  68        if obj.respond_to?(:project) && obj.project
  69          case field_format
  70          when 'user'
  71            obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
  72          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        else
  78          []
  79        end
  80      when 'bool'
  81        [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
  82      else
  83        read_possible_values_utf8_encoded || []
  84      end
  85    end
  86 
  87    def possible_values(obj=nil)
  88      case field_format
  89      when 'user', 'version'
  90        possible_values_options(obj).collect(&:last)
  91      when 'bool'
  92        ['1', '0']
  93      else
  94        read_possible_values_utf8_encoded
  95      end
  96    end
  97 
  98    # Makes possible_values accept a multiline string
  99    def possible_values=(arg)
 100      if arg.is_a?(Array)
 101        write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
 102      else
 103        self.possible_values = arg.to_s.split(/[\n\r]+/)
 104      end
 105    end
 106 
 107    def cast_value(value)
 108      casted = nil
 109      unless value.blank?
 110        case field_format
 111        when 'string', 'text', 'list'
 112          casted = value
 113        when 'date'
 114          casted = begin; value.to_date; rescue; nil end
 115        when 'bool'
 116          casted = (value == '1' ? true : false)
 117        when 'int'
 118          casted = value.to_i
 119        when 'float'
 120          casted = value.to_f
 121        when 'user', 'version'
 122          casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
 123        end
 124      end
 125      casted
 126    end
 127 
 128    # Returns a ORDER BY clause that can used to sort customized
 129    # objects by their value of the custom field.
 130    # Returns false, if the custom field can not be used for sorting.
 131    def order_statement
 132      return nil if multiple?
 133      case field_format
 134        when 'string', 'text', 'list', 'date', 'bool'
 135          # COALESCE is here to make sure that blank and NULL values are sorted equally
 136          "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
 137            " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
 138            " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
 139            " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
 140        when 'int', 'float'
 141          # Make the database cast values into numeric
 142          # Postgresql will raise an error if a value can not be casted!
 143          # CustomValue validations should ensure that it doesn't occur
 144          "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
 145            " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
 146            " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
 147            " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
 148        else
 149          nil
 150      end
 151    end
 152 
 153    def <=>(field)
 154      position <=> field.position
 155    end
 156 
 157    def self.customized_class
 158      self.name =~ /^(.+)CustomField$/
 159      begin; $1.constantize; rescue nil; end
 160    end
 161 
 162    # to move in project_custom_field
 163    def self.for_all
 164      find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
 165    end
 166 
 167    def type_name
 168      nil
 169    end
 170 
 171    # Returns the error messages for the given value
 172    # or an empty array if value is a valid value for the custom field
 173    def validate_field_value(value)
 174      errs = []
 175      if value.is_a?(Array)
 176        if !multiple?
 177          errs << ::I18n.t('activerecord.errors.messages.invalid')
 178        end
 179        if is_required? && value.detect(&:present?).nil?
 180          errs << ::I18n.t('activerecord.errors.messages.blank')
 181        end
 182        value.each {|v| errs += validate_field_value_format(v)}
 183      else
 184        if is_required? && value.blank?
 185          errs << ::I18n.t('activerecord.errors.messages.blank')
 186        end
 187        errs += validate_field_value_format(value)
 188      end
 189      errs
 190    end
 191 
 192    # Returns true if value is a valid value for the custom field
 193    def valid_field_value?(value)
 194      validate_field_value(value).empty?
 195    end
 196 
 197    protected
 198 
 199    # Returns the error message for the given value regarding its format
 200    def validate_field_value_format(value)
 201      errs = []
 202      if value.present?
 203        errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
 204        errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
 205        errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
 206 
 207        # Format specific validations
 208        case field_format
 209        when 'int'
 210          errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
 211        when 'float'
 212          begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
 213        when 'date'
 214          errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
 215        when 'list'
 216          errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
 217        end
 218      end
 219      errs
 220    end
 221 
 222    def read_possible_values_utf8_encoded
 223      values = read_attribute(:possible_values)
 224      if values.is_a?(Array)
 225        values.each do |value|
 226          value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
 227        end
 228      end
 229      values
 230    end
 231  end