aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model/errors.rb
blob: a4cf7002316eb44dc6369eaf97f8c9e82bc2f444 (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
require 'active_support/core_ext/string/inflections'

module ActiveModel
  class Errors < Hash
    include DeprecatedErrorMethods

    def initialize(base)
      @base = base
      super()
    end

    alias_method :get, :[]
    alias_method :set, :[]=

    def [](attribute)
      if errors = get(attribute.to_sym)
        errors
      else
        set(attribute.to_sym, [])
      end
    end

    def []=(attribute, error)
      self[attribute.to_sym] << error
    end

    def each
      each_key do |attribute|
        self[attribute].each { |error| yield attribute, error }
      end
    end

    def size
      values.flatten.size
    end

    def to_a
      full_messages
    end

    def count
      to_a.size
    end

    def to_xml(options={})
      require 'builder' unless defined? ::Builder
      options[:root]    ||= "errors"
      options[:indent]  ||= 2
      options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])

      options[:builder].instruct! unless options.delete(:skip_instruct)
      options[:builder].errors do |e|
        to_a.each { |error| e.error(error) }
      end
    end

    # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
    # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
    # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
    # If no +messsage+ is supplied, :invalid is assumed.
    # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
    def add(attribute, message = nil, options = {})
      message ||= :invalid
      message = generate_message(attribute, message, options) if message.is_a?(Symbol)
      self[attribute] << message
    end

    # Will add an error message to each of the attributes in +attributes+ that is empty.
    def add_on_empty(attributes, custom_message = nil)
      [attributes].flatten.each do |attribute|
        value = @base.send(attribute)
        is_empty = value.respond_to?(:empty?) ? value.empty? : false
        add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty
      end
    end

    # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
    def add_on_blank(attributes, custom_message = nil)
      [attributes].flatten.each do |attribute|
        value = @base.send(attribute)
        add(attribute, :blank, :default => custom_message) if value.blank?
      end
    end

    # Returns all the full error messages in an array.
    #
    #   class Company
    #     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 = attribute.to_s.humanize
          prefix = attr_name + I18n.t('activemodel.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>activemodel.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>activemodel.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>activemodel.errors.models.admin.attributes.title.blank</tt></li>
    # <li><tt>activemodel.errors.models.admin.blank</tt></li>
    # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li>
    # <li><tt>activemodel.errors.models.user.blank</tt></li>
    # <li><tt>activemodel.errors.messages.blank</tt></li>
    # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
    # </ol>
    def generate_message(attribute, message = :invalid, options = {})
      message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)

      klass_ancestors = [@base.class]
      klass_ancestors += @base.class.ancestors.reject {|x| x.is_a?(Module)}

      defaults = klass_ancestors.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.send(attribute)

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

      I18n.translate(key, options)
    end
  end
end