activemodel 3.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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