aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/validations.rb
blob: 7ac6f6fe3bcb50ea7861c07bb088a11d2a3768c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
require 'active_support/core_ext/integer/even_odd'

module ActiveRecord
  # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid.  Use the
  # +record+ method to retrieve the record which did not validate.
  #   begin
  #     complex_operation_that_calls_save!_internally
  #   rescue ActiveRecord::RecordInvalid => invalid
  #     puts invalid.record.errors
  #   end
  class RecordInvalid < ActiveRecordError
    attr_reader :record
    def initialize(record)
      @record = record
      super("Validation failed: #{@record.errors.full_messages.join(", ")}")
    end
  end

  class Errors < ActiveModel::Errors
    class << self
      def default_error_messages
        message = "Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages')."
        ActiveSupport::Deprecation.warn(message)

        I18n.translate 'activerecord.errors.messages'
      end
    end

    # Returns all the full error messages in an array.
    #
    #   class Company < ActiveRecord::Base
    #     validates_presence_of :name, :address, :email
    #     validates_length_of :name, :in => 5..30
    #   end
    #
    #   company = Company.create(:address => '123 First St.')
    #   company.errors.full_messages # =>
    #     ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
    def full_messages(options = {})
      full_messages = []

      each do |attribute, messages|
        messages = Array.wrap(messages)
        next if messages.empty?

        if attribute == :base
          messages.each {|m| full_messages << m }
        else
          attr_name = @base.class.human_attribute_name(attribute.to_s)
          prefix = attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ')
          messages.each do |m|
            full_messages <<  "#{prefix}#{m}"
          end
        end
      end

      full_messages
    end

    # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
    # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, 
    # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the 
    # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name, 
    # translated attribute name and the value are available for interpolation.
    #
    # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
    # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
    # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
    # 
    # <ol>
    # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
    # <li><tt>activerecord.errors.models.admin.blank</tt></li>
    # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
    # <li><tt>activerecord.errors.models.user.blank</tt></li>
    # <li><tt>activerecord.errors.messages.blank</tt></li>
    # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
    # </ol>
    def generate_message(attribute, message = :invalid, options = {})
      message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)

      defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
        [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", 
          :"models.#{klass.name.underscore}.#{message}" ]
      end
      
      defaults << options.delete(:default)
      defaults = defaults.compact.flatten << :"messages.#{message}"

      key = defaults.shift
      value = @base.respond_to?(attribute) ? @base.send(attribute) : nil

      options = { :default => defaults,
        :model => @base.class.human_name,
        :attribute => @base.class.human_attribute_name(attribute.to_s),
        :value => value,
        :scope => [:activerecord, :errors]
      }.merge(options)

      I18n.translate(key, options)
    end
  end

  module Validations
    extend ActiveSupport::Concern

    include ActiveSupport::Callbacks
    include ActiveModel::Validations

    included do
      alias_method_chain :save, :validation
      alias_method_chain :save!, :validation

      define_callbacks :validate_on_create, :validate_on_update
    end

    module ClassMethods
      # Creates an object just like Base.create but calls save! instead of save
      # so an exception is raised if the record is invalid.
      def create!(attributes = nil, &block)
        if attributes.is_a?(Array)
          attributes.collect { |attr| create!(attr, &block) }
        else
          object = new(attributes)
          yield(object) if block_given?
          object.save!
          object
        end
      end

      def validation_method(on)
        case on
        when :create
          :validate_on_create
        when :update
          :validate_on_update
        else
          :validate
        end
      end
    end

    module InstanceMethods
      # The validation process on save can be skipped by passing false. The regular Base#save method is
      # replaced with this when the validations module is mixed in, which it is by default.
      def save_with_validation(perform_validation = true)
        if perform_validation && valid? || !perform_validation
          save_without_validation
        else
          false
        end
      end

      # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
      # if the record is not valid.
      def save_with_validation!
        if valid?
          save_without_validation!
        else
          raise RecordInvalid.new(self)
        end
      end

      # Runs all the specified validations and returns true if no errors were added otherwise false.
      def valid?
        errors.clear

        run_callbacks(:validate)

        if respond_to?(:validate)
          ActiveSupport::Deprecation.warn("Base#validate has been deprecated, please use Base.validate :method instead")
          validate
        end

        if new_record?
          run_callbacks(:validate_on_create)

          if respond_to?(:validate_on_create)
            ActiveSupport::Deprecation.warn("Base#validate_on_create has been deprecated, please use Base.validate_on_create :method instead")
            validate_on_create
          end
        else
          run_callbacks(:validate_on_update)

          if respond_to?(:validate_on_update)
            ActiveSupport::Deprecation.warn("Base#validate_on_update has been deprecated, please use Base.validate_on_update :method instead")
            validate_on_update
          end
        end

        errors.empty?
      end

      # Returns the Errors object that holds all information about attribute error messages.
      def errors
        @errors ||= Errors.new(self)
      end
    end
  end
end

Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path|
  filename = File.basename(path)
  require "active_record/validations/#{filename}"
end