activemodel 3.0.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. data/CHANGELOG +13 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +216 -0
  4. data/lib/active_model.rb +61 -0
  5. data/lib/active_model/attribute_methods.rb +391 -0
  6. data/lib/active_model/callbacks.rb +133 -0
  7. data/lib/active_model/conversion.rb +19 -0
  8. data/lib/active_model/deprecated_error_methods.rb +33 -0
  9. data/lib/active_model/dirty.rb +164 -0
  10. data/lib/active_model/errors.rb +277 -0
  11. data/lib/active_model/lint.rb +89 -0
  12. data/lib/active_model/locale/en.yml +26 -0
  13. data/lib/active_model/naming.rb +60 -0
  14. data/lib/active_model/observing.rb +196 -0
  15. data/lib/active_model/railtie.rb +2 -0
  16. data/lib/active_model/serialization.rb +87 -0
  17. data/lib/active_model/serializers/json.rb +96 -0
  18. data/lib/active_model/serializers/xml.rb +204 -0
  19. data/lib/active_model/test_case.rb +16 -0
  20. data/lib/active_model/translation.rb +60 -0
  21. data/lib/active_model/validations.rb +168 -0
  22. data/lib/active_model/validations/acceptance.rb +51 -0
  23. data/lib/active_model/validations/confirmation.rb +49 -0
  24. data/lib/active_model/validations/exclusion.rb +40 -0
  25. data/lib/active_model/validations/format.rb +64 -0
  26. data/lib/active_model/validations/inclusion.rb +40 -0
  27. data/lib/active_model/validations/length.rb +98 -0
  28. data/lib/active_model/validations/numericality.rb +111 -0
  29. data/lib/active_model/validations/presence.rb +41 -0
  30. data/lib/active_model/validations/validates.rb +108 -0
  31. data/lib/active_model/validations/with.rb +70 -0
  32. data/lib/active_model/validator.rb +160 -0
  33. data/lib/active_model/version.rb +9 -0
  34. metadata +96 -0
@@ -0,0 +1,133 @@
1
+ require 'active_support/callbacks'
2
+
3
+ module ActiveModel
4
+ # == Active Model Callbacks
5
+ #
6
+ # Provides an interface for any class to have Active Record like callbacks.
7
+ #
8
+ # Like the Active Record methods, the call back chain is aborted as soon as
9
+ # one of the methods in the chain returns false.
10
+ #
11
+ # First, extend ActiveModel::Callbacks from the class you are creating:
12
+ #
13
+ # class MyModel
14
+ # extend ActiveModel::Callbacks
15
+ # end
16
+ #
17
+ # Then define a list of methods that you want call backs attached to:
18
+ #
19
+ # define_model_callbacks :create, :update
20
+ #
21
+ # This will provide all three standard callbacks (before, around and after) around
22
+ # both the :create and :update methods. To implement, you need to wrap the methods
23
+ # you want call backs on in a block so that the call backs get a chance to fire:
24
+ #
25
+ # def create
26
+ # _run_create_callbacks do
27
+ # # Your create action methods here
28
+ # end
29
+ # end
30
+ #
31
+ # The _run_<method_name>_callbacks methods are dynamically created when you extend
32
+ # the <tt>ActiveModel::Callbacks</tt> module.
33
+ #
34
+ # Then in your class, you can use the +before_create+, +after_create+ and +around_create+
35
+ # methods, just as you would in an Active Record module.
36
+ #
37
+ # before_create :action_before_create
38
+ #
39
+ # def action_before_create
40
+ # # Your code here
41
+ # end
42
+ #
43
+ # You can choose not to have all three callbacks by passing an hash to the
44
+ # define_model_callbacks method.
45
+ #
46
+ # define_model_callbacks :create, :only => :after, :before
47
+ #
48
+ # Would only create the after_create and before_create callback methods in your
49
+ # class.
50
+ module Callbacks
51
+ def self.extended(base)
52
+ base.class_eval do
53
+ include ActiveSupport::Callbacks
54
+ end
55
+ end
56
+
57
+ # define_model_callbacks accepts all options define_callbacks does, in case you
58
+ # want to overwrite a default. Besides that, it also accepts an :only option,
59
+ # where you can choose if you want all types (before, around or after) or just some.
60
+ #
61
+ # define_model_callbacks :initializer, :only => :after
62
+ #
63
+ # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on
64
+ # that method call. To get around this you can call the define_model_callbacks
65
+ # method as many times as you need.
66
+ #
67
+ # define_model_callbacks :create, :only => :after
68
+ # define_model_callbacks :update, :only => :before
69
+ # define_model_callbacks :destroy, :only => :around
70
+ #
71
+ # Would create +after_create+, +before_update+ and +around_destroy+ methods only.
72
+ #
73
+ # You can pass in a class to before_<type>, after_<type> and around_<type>, in which
74
+ # case the call back will call that class's <action>_<type> method passing the object
75
+ # that the callback is being called on.
76
+ #
77
+ # class MyModel
78
+ # extend ActiveModel::Callbacks
79
+ # define_model_callbacks :create
80
+ #
81
+ # before_create AnotherClass
82
+ # end
83
+ #
84
+ # class AnotherClass
85
+ # def self.before_create( obj )
86
+ # # obj is the MyModel instance that the callback is being called on
87
+ # end
88
+ # end
89
+ #
90
+ def define_model_callbacks(*callbacks)
91
+ options = callbacks.extract_options!
92
+ options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options)
93
+
94
+ types = Array(options.delete(:only))
95
+ types = [:before, :around, :after] if types.empty?
96
+
97
+ callbacks.each do |callback|
98
+ define_callbacks(callback, options)
99
+
100
+ types.each do |type|
101
+ send(:"_define_#{type}_model_callback", self, callback)
102
+ end
103
+ end
104
+ end
105
+
106
+ def _define_before_model_callback(klass, callback) #:nodoc:
107
+ klass.class_eval <<-CALLBACK, __FILE__, __LINE__
108
+ def self.before_#{callback}(*args, &block)
109
+ set_callback(:#{callback}, :before, *args, &block)
110
+ end
111
+ CALLBACK
112
+ end
113
+
114
+ def _define_around_model_callback(klass, callback) #:nodoc:
115
+ klass.class_eval <<-CALLBACK, __FILE__, __LINE__
116
+ def self.around_#{callback}(*args, &block)
117
+ set_callback(:#{callback}, :around, *args, &block)
118
+ end
119
+ CALLBACK
120
+ end
121
+
122
+ def _define_after_model_callback(klass, callback) #:nodoc:
123
+ klass.class_eval <<-CALLBACK, __FILE__, __LINE__
124
+ def self.after_#{callback}(*args, &block)
125
+ options = args.extract_options!
126
+ options[:prepend] = true
127
+ options[:if] = Array(options[:if]) << "!halted && value != false"
128
+ set_callback(:#{callback}, :after, *(args << options), &block)
129
+ end
130
+ CALLBACK
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveModel
2
+ # If your object is already designed to implement all of the Active Model featurs
3
+ # include this module in your Class.
4
+ #
5
+ # class MyClass
6
+ # include ActiveModel::Conversion
7
+ # end
8
+ #
9
+ # Returns self to the <tt>:to_model</tt> method.
10
+ #
11
+ # If your model does not act like an Active Model object, then you should define
12
+ # <tt>:to_model</tt> yourself returning a proxy object that wraps your object
13
+ # with Active Model compliant methods.
14
+ module Conversion
15
+ def to_model
16
+ self
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module ActiveModel
2
+ module DeprecatedErrorMethods
3
+ def on(attribute)
4
+ message = "Errors#on have been deprecated, use Errors#[] instead.\n"
5
+ message << "Also note that the behaviour of Errors#[] has changed. Errors#[] now always returns an Array. An empty Array is "
6
+ message << "returned when there are no errors on the specified attribute."
7
+ ActiveSupport::Deprecation.warn(message)
8
+
9
+ errors = self[attribute]
10
+ errors.size < 2 ? errors.first : errors
11
+ end
12
+
13
+ def on_base
14
+ ActiveSupport::Deprecation.warn "Errors#on_base have been deprecated, use Errors#[:base] instead"
15
+ ActiveSupport::Deprecation.silence { on(:base) }
16
+ end
17
+
18
+ def add_to_base(msg)
19
+ ActiveSupport::Deprecation.warn "Errors#add_to_base(msg) has been deprecated, use Errors#[:base] << msg instead"
20
+ self[:base] << msg
21
+ end
22
+
23
+ def invalid?(attribute)
24
+ ActiveSupport::Deprecation.warn "Errors#invalid?(attribute) has been deprecated, use Errors#[attribute].any? instead"
25
+ self[attribute].any?
26
+ end
27
+
28
+ def each_full
29
+ ActiveSupport::Deprecation.warn "Errors#each_full has been deprecated, use Errors#to_a.each instead"
30
+ to_a.each { |error| yield error }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,164 @@
1
+ module ActiveModel
2
+ # <tt>ActiveModel::Dirty</tt> provides a way to track changes in your
3
+ # object in the same way as ActiveRecord does.
4
+ #
5
+ # The requirements to implement ActiveModel::Dirty are:
6
+ #
7
+ # * <tt>include ActiveModel::Dirty</tt> in your object
8
+ # * Call <tt>define_attribute_methods</tt> passing each method you want to track
9
+ # * Call <tt>attr_name_will_change!</tt> before each change to the tracked attribute
10
+ #
11
+ # If you wish to also track previous changes on save or update, you need to add
12
+ #
13
+ # @previously_changed = changes
14
+ #
15
+ # inside of your save or update method.
16
+ #
17
+ # A minimal implementation could be:
18
+ #
19
+ # class Person
20
+ #
21
+ # include ActiveModel::Dirty
22
+ #
23
+ # define_attribute_methods [:name]
24
+ #
25
+ # def name
26
+ # @name
27
+ # end
28
+ #
29
+ # def name=(val)
30
+ # name_will_change!
31
+ # @name = val
32
+ # end
33
+ #
34
+ # def save
35
+ # @previously_changed = changes
36
+ # end
37
+ #
38
+ # end
39
+ #
40
+ # == Examples:
41
+ #
42
+ # A newly instantiated object is unchanged:
43
+ # person = Person.find_by_name('Uncle Bob')
44
+ # person.changed? # => false
45
+ #
46
+ # Change the name:
47
+ # person.name = 'Bob'
48
+ # person.changed? # => true
49
+ # person.name_changed? # => true
50
+ # person.name_was # => 'Uncle Bob'
51
+ # person.name_change # => ['Uncle Bob', 'Bob']
52
+ # person.name = 'Bill'
53
+ # person.name_change # => ['Uncle Bob', 'Bill']
54
+ #
55
+ # Save the changes:
56
+ # person.save
57
+ # person.changed? # => false
58
+ # person.name_changed? # => false
59
+ #
60
+ # Assigning the same value leaves the attribute unchanged:
61
+ # person.name = 'Bill'
62
+ # person.name_changed? # => false
63
+ # person.name_change # => nil
64
+ #
65
+ # Which attributes have changed?
66
+ # person.name = 'Bob'
67
+ # person.changed # => ['name']
68
+ # person.changes # => { 'name' => ['Bill', 'Bob'] }
69
+ #
70
+ # Resetting an attribute returns it to its original state:
71
+ # person.reset_name! # => 'Bill'
72
+ # person.changed? # => false
73
+ # person.name_changed? # => false
74
+ # person.name # => 'Bill'
75
+ #
76
+ # Before modifying an attribute in-place:
77
+ # person.name_will_change!
78
+ # person.name << 'y'
79
+ # person.name_change # => ['Bill', 'Billy']
80
+ module Dirty
81
+ extend ActiveSupport::Concern
82
+ include ActiveModel::AttributeMethods
83
+
84
+ included do
85
+ attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
86
+ attribute_method_affix :prefix => 'reset_', :suffix => '!'
87
+ end
88
+
89
+ # Do any attributes have unsaved changes?
90
+ # person.changed? # => false
91
+ # person.name = 'bob'
92
+ # person.changed? # => true
93
+ def changed?
94
+ !changed_attributes.empty?
95
+ end
96
+
97
+ # List of attributes with unsaved changes.
98
+ # person.changed # => []
99
+ # person.name = 'bob'
100
+ # person.changed # => ['name']
101
+ def changed
102
+ changed_attributes.keys
103
+ end
104
+
105
+ # Map of changed attrs => [original value, new value].
106
+ # person.changes # => {}
107
+ # person.name = 'bob'
108
+ # person.changes # => { 'name' => ['bill', 'bob'] }
109
+ def changes
110
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
111
+ end
112
+
113
+ # Map of attributes that were changed when the model was saved.
114
+ # person.name # => 'bob'
115
+ # person.name = 'robert'
116
+ # person.save
117
+ # person.previous_changes # => {'name' => ['bob, 'robert']}
118
+ def previous_changes
119
+ previously_changed_attributes
120
+ end
121
+
122
+ private
123
+ # Map of change <tt>attr => original value</tt>.
124
+ def changed_attributes
125
+ @changed_attributes ||= {}
126
+ end
127
+
128
+ # Map of fields that were changed when the model was saved
129
+ def previously_changed_attributes
130
+ @previously_changed || {}
131
+ end
132
+
133
+ # Handle <tt>*_changed?</tt> for +method_missing+.
134
+ def attribute_changed?(attr)
135
+ changed_attributes.include?(attr)
136
+ end
137
+
138
+ # Handle <tt>*_change</tt> for +method_missing+.
139
+ def attribute_change(attr)
140
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
141
+ end
142
+
143
+ # Handle <tt>*_was</tt> for +method_missing+.
144
+ def attribute_was(attr)
145
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
146
+ end
147
+
148
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
149
+ def attribute_will_change!(attr)
150
+ begin
151
+ value = __send__(attr)
152
+ value = value.duplicable? ? value.clone : value
153
+ rescue TypeError, NoMethodError
154
+ end
155
+
156
+ changed_attributes[attr] = value
157
+ end
158
+
159
+ # Handle <tt>reset_*!</tt> for +method_missing+.
160
+ def reset_attribute!(attr)
161
+ __send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr)
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,277 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'active_support/ordered_hash'
3
+
4
+ module ActiveModel
5
+ # Provides a modified +OrderedHash+ that you can include in your object
6
+ # for handling error messages and interacting with Action Pack helpers.
7
+ #
8
+ # A minimal implementation could be:
9
+ #
10
+ # class Person
11
+ #
12
+ # # Required dependency for ActiveModel::Errors
13
+ # extend ActiveModel::Naming
14
+ #
15
+ # def initialize
16
+ # @errors = ActiveModel::Errors.new(self)
17
+ # end
18
+ #
19
+ # attr_accessor :name
20
+ # attr_reader :errors
21
+ #
22
+ # def validate!
23
+ # errors.add(:name, "can not be nil") if name == nil
24
+ # end
25
+ #
26
+ # # The following methods are needed to be minimally implemented
27
+ #
28
+ # def read_attribute_for_validation(attr)
29
+ # send(attr)
30
+ # end
31
+ #
32
+ # def ErrorsPerson.human_attribute_name(attr, options = {})
33
+ # attr
34
+ # end
35
+ #
36
+ # def ErrorsPerson.lookup_ancestors
37
+ # [self]
38
+ # end
39
+ #
40
+ # end
41
+ #
42
+ # The last three methods are required in your object for Errors to be
43
+ # able to generate error messages correctly and also handle multiple
44
+ # languages. Of course, if you extend your object with ActiveModel::Translations
45
+ # you will not need to implement the last two. Likewise, using
46
+ # ActiveModel::Validations will handle the validation related methods
47
+ # for you.
48
+ #
49
+ # The above allows you to do:
50
+ #
51
+ # p = Person.new
52
+ # p.validate! # => ["can not be nil"]
53
+ # p.errors.full_messages # => ["name can not be nil"]
54
+ # # etc..
55
+ class Errors < ActiveSupport::OrderedHash
56
+ include DeprecatedErrorMethods
57
+
58
+ # Pass in the instance of the object that is using the errors object.
59
+ #
60
+ # class Person
61
+ # def initialize
62
+ # @errors = ActiveModel::Errors.new(self)
63
+ # end
64
+ # end
65
+ def initialize(base)
66
+ @base = base
67
+ super()
68
+ end
69
+
70
+ alias_method :get, :[]
71
+ alias_method :set, :[]=
72
+
73
+ # When passed a symbol or a name of a method, returns an array of errors for the method.
74
+ #
75
+ # p.errors[:name] #=> ["can not be nil"]
76
+ # p.errors['name'] #=> ["can not be nil"]
77
+ def [](attribute)
78
+ if errors = get(attribute.to_sym)
79
+ errors
80
+ else
81
+ set(attribute.to_sym, [])
82
+ end
83
+ end
84
+
85
+ # Adds to the supplied attribute the supplied error message.
86
+ #
87
+ # p.errors[:name] = "must be set"
88
+ # p.errors[:name] #=> ['must be set']
89
+ def []=(attribute, error)
90
+ self[attribute.to_sym] << error
91
+ end
92
+
93
+ # Iterates through each error key, value pair in the error messages hash.
94
+ # Yields the attribute and the error for that attribute. If the attribute
95
+ # has more than one error message, yields once for each error message.
96
+ #
97
+ # p.errors.add(:name, "can't be blank")
98
+ # p.errors.each do |attribute, errors_array|
99
+ # # Will yield :name and "can't be blank"
100
+ # end
101
+ #
102
+ # p.errors.add(:name, "must be specified")
103
+ # p.errors.each do |attribute, errors_array|
104
+ # # Will yield :name and "can't be blank"
105
+ # # then yield :name and "must be specified"
106
+ # end
107
+ def each
108
+ each_key do |attribute|
109
+ self[attribute].each { |error| yield attribute, error }
110
+ end
111
+ end
112
+
113
+ # Returns the number of error messages.
114
+ #
115
+ # p.errors.add(:name, "can't be blank")
116
+ # p.errors.size #=> 1
117
+ # p.errors.add(:name, "must be specified")
118
+ # p.errors.size #=> 2
119
+ def size
120
+ values.flatten.size
121
+ end
122
+
123
+ # Returns an array of error messages, with the attribute name included
124
+ #
125
+ # p.errors.add(:name, "can't be blank")
126
+ # p.errors.add(:name, "must be specified")
127
+ # p.errors.to_a #=> ["name can't be blank", "name must be specified"]
128
+ def to_a
129
+ full_messages
130
+ end
131
+
132
+ # Returns the number of error messages.
133
+ # p.errors.add(:name, "can't be blank")
134
+ # p.errors.count #=> 1
135
+ # p.errors.add(:name, "must be specified")
136
+ # p.errors.count #=> 2
137
+ def count
138
+ to_a.size
139
+ end
140
+
141
+ # Returns an xml formatted representation of the Errors hash.
142
+ #
143
+ # p.errors.add(:name, "can't be blank")
144
+ # p.errors.add(:name, "must be specified")
145
+ # p.errors.to_xml #=> Produces:
146
+ #
147
+ # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
148
+ # # <errors>
149
+ # # <error>name can't be blank</error>
150
+ # # <error>name must be specified</error>
151
+ # # </errors>
152
+ def to_xml(options={})
153
+ require 'builder' unless defined? ::Builder
154
+ options[:root] ||= "errors"
155
+ options[:indent] ||= 2
156
+ options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
157
+
158
+ options[:builder].instruct! unless options.delete(:skip_instruct)
159
+ options[:builder].errors do |e|
160
+ to_a.each { |error| e.error(error) }
161
+ end
162
+ end
163
+
164
+ # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
165
+ # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
166
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
167
+ # If no +messsage+ is supplied, :invalid is assumed.
168
+ #
169
+ # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
170
+ # If +message+ is a Proc, it will be called, allowing for things like Time.now to be used within an error
171
+ def add(attribute, message = nil, options = {})
172
+ message ||= :invalid
173
+ message = generate_message(attribute, message, options) if message.is_a?(Symbol)
174
+ message = message.call if message.is_a?(Proc)
175
+ self[attribute] << message
176
+ end
177
+
178
+ # Will add an error message to each of the attributes in +attributes+ that is empty.
179
+ def add_on_empty(attributes, custom_message = nil)
180
+ [attributes].flatten.each do |attribute|
181
+ value = @base.send(:read_attribute_for_validation, attribute)
182
+ is_empty = value.respond_to?(:empty?) ? value.empty? : false
183
+ add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty
184
+ end
185
+ end
186
+
187
+ # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
188
+ def add_on_blank(attributes, custom_message = nil)
189
+ [attributes].flatten.each do |attribute|
190
+ value = @base.send(:read_attribute_for_validation, attribute)
191
+ add(attribute, :blank, :default => custom_message) if value.blank?
192
+ end
193
+ end
194
+
195
+ # Returns all the full error messages in an array.
196
+ #
197
+ # class Company
198
+ # validates_presence_of :name, :address, :email
199
+ # validates_length_of :name, :in => 5..30
200
+ # end
201
+ #
202
+ # company = Company.create(:address => '123 First St.')
203
+ # company.errors.full_messages # =>
204
+ # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
205
+ def full_messages
206
+ full_messages = []
207
+
208
+ each do |attribute, messages|
209
+ messages = Array(messages)
210
+ next if messages.empty?
211
+
212
+ if attribute == :base
213
+ messages.each {|m| full_messages << m }
214
+ else
215
+ attr_name = attribute.to_s.gsub('.', '_').humanize
216
+ attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
217
+ options = { :default => "{{attribute}} {{message}}", :attribute => attr_name }
218
+
219
+ messages.each do |m|
220
+ full_messages << I18n.t(:"errors.format", options.merge(:message => m))
221
+ end
222
+ end
223
+ end
224
+
225
+ full_messages
226
+ end
227
+
228
+ # Translates an error message in its default scope (<tt>activemodel.errors.messages</tt>).
229
+ # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
230
+ # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
231
+ # default message (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model name,
232
+ # translated attribute name and the value are available for interpolation.
233
+ #
234
+ # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
235
+ # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
236
+ # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
237
+ #
238
+ # <ol>
239
+ # <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li>
240
+ # <li><tt>activemodel.errors.models.admin.blank</tt></li>
241
+ # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li>
242
+ # <li><tt>activemodel.errors.models.user.blank</tt></li>
243
+ # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
244
+ # <li><tt>activemodel.errors.messages.blank</tt></li>
245
+ # <li><tt>errors.attributes.title.blank</tt></li>
246
+ # <li><tt>errors.messages.blank</tt></li>
247
+ # </ol>
248
+ def generate_message(attribute, message = :invalid, options = {})
249
+ message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
250
+
251
+ defaults = @base.class.lookup_ancestors.map do |klass|
252
+ [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{message}",
253
+ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{message}" ]
254
+ end
255
+
256
+ defaults << options.delete(:default)
257
+ defaults << :"#{@base.class.i18n_scope}.errors.messages.#{message}"
258
+ defaults << :"errors.attributes.#{attribute}.#{message}"
259
+ defaults << :"errors.messages.#{message}"
260
+
261
+ defaults.compact!
262
+ defaults.flatten!
263
+
264
+ key = defaults.shift
265
+ value = @base.send(:read_attribute_for_validation, attribute)
266
+
267
+ options = {
268
+ :default => defaults,
269
+ :model => @base.class.model_name.human,
270
+ :attribute => @base.class.human_attribute_name(attribute),
271
+ :value => value
272
+ }.merge(options)
273
+
274
+ I18n.translate(key, options)
275
+ end
276
+ end
277
+ end