activemodel 3.0.0.beta4 → 3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG +1 -39
  2. data/MIT-LICENSE +1 -1
  3. data/README +16 -200
  4. data/lib/active_model.rb +19 -28
  5. data/lib/active_model/attribute_methods.rb +27 -142
  6. data/lib/active_model/conversion.rb +1 -37
  7. data/lib/active_model/dirty.rb +12 -51
  8. data/lib/active_model/errors.rb +22 -146
  9. data/lib/active_model/lint.rb +14 -48
  10. data/lib/active_model/locale/en.yml +23 -26
  11. data/lib/active_model/naming.rb +5 -41
  12. data/lib/active_model/observing.rb +16 -35
  13. data/lib/active_model/serialization.rb +0 -57
  14. data/lib/active_model/serializers/json.rb +8 -13
  15. data/lib/active_model/serializers/xml.rb +123 -63
  16. data/lib/active_model/state_machine.rb +70 -0
  17. data/lib/active_model/state_machine/event.rb +62 -0
  18. data/lib/active_model/state_machine/machine.rb +75 -0
  19. data/lib/active_model/state_machine/state.rb +47 -0
  20. data/lib/active_model/state_machine/state_transition.rb +40 -0
  21. data/lib/active_model/test_case.rb +2 -0
  22. data/lib/active_model/validations.rb +62 -125
  23. data/lib/active_model/validations/acceptance.rb +18 -23
  24. data/lib/active_model/validations/confirmation.rb +10 -14
  25. data/lib/active_model/validations/exclusion.rb +13 -15
  26. data/lib/active_model/validations/format.rb +24 -26
  27. data/lib/active_model/validations/inclusion.rb +13 -15
  28. data/lib/active_model/validations/length.rb +65 -61
  29. data/lib/active_model/validations/numericality.rb +58 -76
  30. data/lib/active_model/validations/presence.rb +8 -8
  31. data/lib/active_model/validations/with.rb +22 -90
  32. data/lib/active_model/validations_repair_helper.rb +35 -0
  33. data/lib/active_model/version.rb +2 -3
  34. metadata +19 -63
  35. data/lib/active_model/callbacks.rb +0 -134
  36. data/lib/active_model/railtie.rb +0 -2
  37. data/lib/active_model/translation.rb +0 -60
  38. data/lib/active_model/validations/validates.rb +0 -108
  39. data/lib/active_model/validator.rb +0 -183
@@ -1,44 +1,8 @@
1
1
  module ActiveModel
2
- # Handle default conversions: to_model, to_key and to_param.
3
- #
4
- # == Example
5
- #
6
- # Let's take for example this non persisted object.
7
- #
8
- # class ContactMessage
9
- # include ActiveModel::Conversion
10
- #
11
- # # ContactMessage are never persisted in the DB
12
- # def persisted?
13
- # false
14
- # end
15
- # end
16
- #
17
- # cm = ContactMessage.new
18
- # cm.to_model == self #=> true
19
- # cm.to_key #=> nil
20
- # cm.to_param #=> nil
21
- #
2
+ # Include ActiveModel::Conversion if your object "acts like an ActiveModel model".
22
3
  module Conversion
23
- # If your object is already designed to implement all of the Active Model you can use
24
- # the default to_model implementation, which simply returns self.
25
- #
26
- # If your model does not act like an Active Model object, then you should define
27
- # <tt>:to_model</tt> yourself returning a proxy object that wraps your object
28
- # with Active Model compliant methods.
29
4
  def to_model
30
5
  self
31
6
  end
32
-
33
- # Returns an Enumerable of all (primary) key attributes or nil if persisted? is fakse
34
- def to_key
35
- persisted? ? [id] : nil
36
- end
37
-
38
- # Returns a string representing the object's key suitable for use in URLs,
39
- # or nil if persisted? is false
40
- def to_param
41
- to_key ? to_key.join('-') : nil
42
- end
43
7
  end
44
8
  end
@@ -1,48 +1,5 @@
1
- require 'active_model/attribute_methods'
2
- require 'active_support/concern'
3
- require 'active_support/hash_with_indifferent_access'
4
- require 'active_support/core_ext/object/duplicable'
5
-
6
1
  module ActiveModel
7
- # <tt>ActiveModel::Dirty</tt> provides a way to track changes in your
8
- # object in the same way as ActiveRecord does.
9
- #
10
- # The requirements to implement ActiveModel::Dirty are:
11
- #
12
- # * <tt>include ActiveModel::Dirty</tt> in your object
13
- # * Call <tt>define_attribute_methods</tt> passing each method you want to track
14
- # * Call <tt>attr_name_will_change!</tt> before each change to the tracked attribute
15
- #
16
- # If you wish to also track previous changes on save or update, you need to add
17
- #
18
- # @previously_changed = changes
19
- #
20
- # inside of your save or update method.
21
- #
22
- # A minimal implementation could be:
23
- #
24
- # class Person
25
- #
26
- # include ActiveModel::Dirty
27
- #
28
- # define_attribute_methods [:name]
29
- #
30
- # def name
31
- # @name
32
- # end
33
- #
34
- # def name=(val)
35
- # name_will_change!
36
- # @name = val
37
- # end
38
- #
39
- # def save
40
- # @previously_changed = changes
41
- # end
42
- #
43
- # end
44
- #
45
- # == Examples:
2
+ # Track unsaved attribute changes.
46
3
  #
47
4
  # A newly instantiated object is unchanged:
48
5
  # person = Person.find_by_name('Uncle Bob')
@@ -112,7 +69,7 @@ module ActiveModel
112
69
  # person.name = 'bob'
113
70
  # person.changes # => { 'name' => ['bill', 'bob'] }
114
71
  def changes
115
- changed.inject(HashWithIndifferentAccess.new){ |h, attr| h[attr] = attribute_change(attr); h }
72
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
116
73
  end
117
74
 
118
75
  # Map of attributes that were changed when the model was saved.
@@ -121,15 +78,19 @@ module ActiveModel
121
78
  # person.save
122
79
  # person.previous_changes # => {'name' => ['bob, 'robert']}
123
80
  def previous_changes
124
- @previously_changed
125
- end
126
-
127
- # Map of change <tt>attr => original value</tt>.
128
- def changed_attributes
129
- @changed_attributes ||= {}
81
+ previously_changed_attributes
130
82
  end
131
83
 
132
84
  private
85
+ # Map of change <tt>attr => original value</tt>.
86
+ def changed_attributes
87
+ @changed_attributes ||= {}
88
+ end
89
+
90
+ # Map of fields that were changed when the model was saved
91
+ def previously_changed_attributes
92
+ @previously_changed || {}
93
+ end
133
94
 
134
95
  # Handle <tt>*_changed?</tt> for +method_missing+.
135
96
  def attribute_changed?(attr)
@@ -1,71 +1,10 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- require 'active_support/core_ext/array/wrap'
4
1
  require 'active_support/core_ext/string/inflections'
5
- require 'active_support/core_ext/object/blank'
6
2
  require 'active_support/ordered_hash'
7
3
 
8
4
  module ActiveModel
9
- # Provides a modified +OrderedHash+ that you can include in your object
10
- # for handling error messages and interacting with Action Pack helpers.
11
- #
12
- # A minimal implementation could be:
13
- #
14
- # class Person
15
- #
16
- # # Required dependency for ActiveModel::Errors
17
- # extend ActiveModel::Naming
18
- #
19
- # def initialize
20
- # @errors = ActiveModel::Errors.new(self)
21
- # end
22
- #
23
- # attr_accessor :name
24
- # attr_reader :errors
25
- #
26
- # def validate!
27
- # errors.add(:name, "can not be nil") if name == nil
28
- # end
29
- #
30
- # # The following methods are needed to be minimally implemented
31
- #
32
- # def read_attribute_for_validation(attr)
33
- # send(attr)
34
- # end
35
- #
36
- # def ErrorsPerson.human_attribute_name(attr, options = {})
37
- # attr
38
- # end
39
- #
40
- # def ErrorsPerson.lookup_ancestors
41
- # [self]
42
- # end
43
- #
44
- # end
45
- #
46
- # The last three methods are required in your object for Errors to be
47
- # able to generate error messages correctly and also handle multiple
48
- # languages. Of course, if you extend your object with ActiveModel::Translations
49
- # you will not need to implement the last two. Likewise, using
50
- # ActiveModel::Validations will handle the validation related methods
51
- # for you.
52
- #
53
- # The above allows you to do:
54
- #
55
- # p = Person.new
56
- # p.validate! # => ["can not be nil"]
57
- # p.errors.full_messages # => ["name can not be nil"]
58
- # # etc..
59
5
  class Errors < ActiveSupport::OrderedHash
60
6
  include DeprecatedErrorMethods
61
7
 
62
- # Pass in the instance of the object that is using the errors object.
63
- #
64
- # class Person
65
- # def initialize
66
- # @errors = ActiveModel::Errors.new(self)
67
- # end
68
- # end
69
8
  def initialize(base)
70
9
  @base = base
71
10
  super()
@@ -74,10 +13,6 @@ module ActiveModel
74
13
  alias_method :get, :[]
75
14
  alias_method :set, :[]=
76
15
 
77
- # When passed a symbol or a name of a method, returns an array of errors for the method.
78
- #
79
- # p.errors[:name] #=> ["can not be nil"]
80
- # p.errors['name'] #=> ["can not be nil"]
81
16
  def [](attribute)
82
17
  if errors = get(attribute.to_sym)
83
18
  errors
@@ -86,78 +21,28 @@ module ActiveModel
86
21
  end
87
22
  end
88
23
 
89
- # Adds to the supplied attribute the supplied error message.
90
- #
91
- # p.errors[:name] = "must be set"
92
- # p.errors[:name] #=> ['must be set']
93
24
  def []=(attribute, error)
94
25
  self[attribute.to_sym] << error
95
26
  end
96
27
 
97
- # Iterates through each error key, value pair in the error messages hash.
98
- # Yields the attribute and the error for that attribute. If the attribute
99
- # has more than one error message, yields once for each error message.
100
- #
101
- # p.errors.add(:name, "can't be blank")
102
- # p.errors.each do |attribute, errors_array|
103
- # # Will yield :name and "can't be blank"
104
- # end
105
- #
106
- # p.errors.add(:name, "must be specified")
107
- # p.errors.each do |attribute, errors_array|
108
- # # Will yield :name and "can't be blank"
109
- # # then yield :name and "must be specified"
110
- # end
111
28
  def each
112
29
  each_key do |attribute|
113
30
  self[attribute].each { |error| yield attribute, error }
114
31
  end
115
32
  end
116
33
 
117
- # Returns the number of error messages.
118
- #
119
- # p.errors.add(:name, "can't be blank")
120
- # p.errors.size #=> 1
121
- # p.errors.add(:name, "must be specified")
122
- # p.errors.size #=> 2
123
34
  def size
124
35
  values.flatten.size
125
36
  end
126
37
 
127
- # Returns an array of error messages, with the attribute name included
128
- #
129
- # p.errors.add(:name, "can't be blank")
130
- # p.errors.add(:name, "must be specified")
131
- # p.errors.to_a #=> ["name can't be blank", "name must be specified"]
132
38
  def to_a
133
39
  full_messages
134
40
  end
135
41
 
136
- # Returns the number of error messages.
137
- # p.errors.add(:name, "can't be blank")
138
- # p.errors.count #=> 1
139
- # p.errors.add(:name, "must be specified")
140
- # p.errors.count #=> 2
141
42
  def count
142
43
  to_a.size
143
44
  end
144
45
 
145
- # Returns true if there are any errors, false if not.
146
- def empty?
147
- all? { |k, v| v && v.empty? }
148
- end
149
-
150
- # Returns an xml formatted representation of the Errors hash.
151
- #
152
- # p.errors.add(:name, "can't be blank")
153
- # p.errors.add(:name, "must be specified")
154
- # p.errors.to_xml #=> Produces:
155
- #
156
- # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
157
- # # <errors>
158
- # # <error>name can't be blank</error>
159
- # # <error>name must be specified</error>
160
- # # </errors>
161
46
  def to_xml(options={})
162
47
  require 'builder' unless defined? ::Builder
163
48
  options[:root] ||= "errors"
@@ -170,17 +55,14 @@ module ActiveModel
170
55
  end
171
56
  end
172
57
 
173
- # Adds +message+ to the error messages on +attribute+, which will be returned on a call to
174
- # <tt>on(attribute)</tt> for the same attribute. More than one error can be added to the same
175
- # +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
176
- # If no +message+ is supplied, <tt>:invalid</tt> is assumed.
177
- #
178
- # If +message+ is a symbol, it will be translated using the appropriate scope (see +translate_error+).
179
- # If +message+ is a proc, it will be called, allowing for things like <tt>Time.now</tt> to be used within an error.
58
+ # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
59
+ # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
60
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
61
+ # If no +messsage+ is supplied, :invalid is assumed.
62
+ # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
180
63
  def add(attribute, message = nil, options = {})
181
64
  message ||= :invalid
182
65
  message = generate_message(attribute, message, options) if message.is_a?(Symbol)
183
- message = message.call if message.is_a?(Proc)
184
66
  self[attribute] << message
185
67
  end
186
68
 
@@ -211,7 +93,7 @@ module ActiveModel
211
93
  # company = Company.create(:address => '123 First St.')
212
94
  # company.errors.full_messages # =>
213
95
  # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
214
- def full_messages
96
+ def full_messages(options = {})
215
97
  full_messages = []
216
98
 
217
99
  each do |attribute, messages|
@@ -221,12 +103,10 @@ module ActiveModel
221
103
  if attribute == :base
222
104
  messages.each {|m| full_messages << m }
223
105
  else
224
- attr_name = attribute.to_s.gsub('.', '_').humanize
225
- attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
226
- options = { :default => "%{attribute} %{message}", :attribute => attr_name }
227
-
106
+ attr_name = attribute.to_s.humanize
107
+ prefix = attr_name + I18n.t('activemodel.errors.format.separator', :default => ' ')
228
108
  messages.each do |m|
229
- full_messages << I18n.t(:"errors.format", options.merge(:message => m))
109
+ full_messages << "#{prefix}#{m}"
230
110
  end
231
111
  end
232
112
  end
@@ -249,35 +129,31 @@ module ActiveModel
249
129
  # <li><tt>activemodel.errors.models.admin.blank</tt></li>
250
130
  # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li>
251
131
  # <li><tt>activemodel.errors.models.user.blank</tt></li>
252
- # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
253
132
  # <li><tt>activemodel.errors.messages.blank</tt></li>
254
- # <li><tt>errors.attributes.title.blank</tt></li>
255
- # <li><tt>errors.messages.blank</tt></li>
133
+ # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
256
134
  # </ol>
257
135
  def generate_message(attribute, message = :invalid, options = {})
258
136
  message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
259
137
 
260
- defaults = @base.class.lookup_ancestors.map do |klass|
261
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{message}",
262
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{message}" ]
138
+ klass_ancestors = [@base.class]
139
+ klass_ancestors += @base.class.ancestors.reject {|x| x.is_a?(Module)}
140
+
141
+ defaults = klass_ancestors.map do |klass|
142
+ [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
143
+ :"models.#{klass.name.underscore}.#{message}" ]
263
144
  end
264
145
 
265
146
  defaults << options.delete(:default)
266
- defaults << :"#{@base.class.i18n_scope}.errors.messages.#{message}"
267
- defaults << :"errors.attributes.#{attribute}.#{message}"
268
- defaults << :"errors.messages.#{message}"
269
-
270
- defaults.compact!
271
- defaults.flatten!
147
+ defaults = defaults.compact.flatten << :"messages.#{message}"
272
148
 
273
149
  key = defaults.shift
274
150
  value = @base.send(:read_attribute_for_validation, attribute)
275
151
 
276
- options = {
277
- :default => defaults,
278
- :model => @base.class.model_name.human,
279
- :attribute => @base.class.human_attribute_name(attribute),
280
- :value => value
152
+ options = { :default => defaults,
153
+ :model => @base.class.name.humanize,
154
+ :attribute => attribute.to_s.humanize,
155
+ :value => value,
156
+ :scope => [:activemodel, :errors]
281
157
  }.merge(options)
282
158
 
283
159
  I18n.translate(key, options)
@@ -13,34 +13,8 @@
13
13
  module ActiveModel
14
14
  module Lint
15
15
  module Tests
16
-
17
- # == Responds to <tt>to_key</tt>
18
- #
19
- # Returns an Enumerable of all (primary) key attributes
20
- # or nil if model.persisted? is false
21
- def test_to_key
22
- assert model.respond_to?(:to_key), "The model should respond to to_key"
23
- def model.persisted?() false end
24
- assert model.to_key.nil?
25
- end
26
-
27
- # == Responds to <tt>to_param</tt>
28
- #
29
- # Returns a string representing the object's key suitable for use in URLs
30
- # or nil if model.persisted? is false.
31
- #
32
- # Implementers can decide to either raise an exception or provide a default
33
- # in case the record uses a composite primary key. There are no tests for this
34
- # behavior in lint because it doesn't make sense to force any of the possible
35
- # implementation strategies on the implementer. However, if the resource is
36
- # not persisted?, then to_param should always return nil.
37
- def test_to_param
38
- assert model.respond_to?(:to_param), "The model should respond to to_param"
39
- def model.persisted?() false end
40
- assert model.to_param.nil?
41
- end
42
-
43
- # == Responds to <tt>valid?</tt>
16
+ # valid?
17
+ # ------
44
18
  #
45
19
  # Returns a boolean that specifies whether the object is in a valid or invalid
46
20
  # state.
@@ -49,38 +23,30 @@ module ActiveModel
49
23
  assert_boolean model.valid?, "valid?"
50
24
  end
51
25
 
52
- # == Responds to <tt>persisted?</tt>
26
+ # new_record?
27
+ # -----------
53
28
  #
54
29
  # Returns a boolean that specifies whether the object has been persisted yet.
55
30
  # This is used when calculating the URL for an object. If the object is
56
31
  # not persisted, a form for that object, for instance, will be POSTed to the
57
32
  # collection. If it is persisted, a form for the object will put PUTed to the
58
33
  # URL for the object.
59
- def test_persisted?
60
- assert model.respond_to?(:persisted?), "The model should respond to persisted?"
61
- assert_boolean model.persisted?, "persisted?"
34
+ def test_new_record?
35
+ assert model.respond_to?(:new_record?), "The model should respond to new_record?"
36
+ assert_boolean model.new_record?, "new_record?"
62
37
  end
63
38
 
64
- # == Naming
65
- #
66
- # Model.model_name must returns a string with some convenience methods as
67
- # :human and :partial_path. Check ActiveModel::Naming for more information.
68
- #
69
- def test_model_naming
70
- assert model.class.respond_to?(:model_name), "The model should respond to model_name"
71
- model_name = model.class.model_name
72
- assert_kind_of String, model_name
73
- assert_kind_of String, model_name.human
74
- assert_kind_of String, model_name.partial_path
75
- assert_kind_of String, model_name.singular
76
- assert_kind_of String, model_name.plural
39
+ def test_destroyed?
40
+ assert model.respond_to?(:destroyed?), "The model should respond to destroyed?"
41
+ assert_boolean model.destroyed?, "destroyed?"
77
42
  end
78
43
 
79
- # == Errors Testing
80
- #
44
+ # errors
45
+ # ------
46
+ #
81
47
  # Returns an object that has :[] and :full_messages defined on it. See below
82
48
  # for more details.
83
- #
49
+
84
50
  # Returns an Array of Strings that are the errors for the attribute in
85
51
  # question. If localization is used, the Strings should be localized
86
52
  # for the current locale. If no error is present, this method should