activemodel 5.2.3
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +114 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +264 -0
- data/lib/active_model.rb +77 -0
- data/lib/active_model/attribute.rb +248 -0
- data/lib/active_model/attribute/user_provided_default.rb +52 -0
- data/lib/active_model/attribute_assignment.rb +57 -0
- data/lib/active_model/attribute_methods.rb +478 -0
- data/lib/active_model/attribute_mutation_tracker.rb +124 -0
- data/lib/active_model/attribute_set.rb +114 -0
- data/lib/active_model/attribute_set/builder.rb +126 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
- data/lib/active_model/attributes.rb +111 -0
- data/lib/active_model/callbacks.rb +153 -0
- data/lib/active_model/conversion.rb +111 -0
- data/lib/active_model/dirty.rb +343 -0
- data/lib/active_model/errors.rb +517 -0
- data/lib/active_model/forbidden_attributes_protection.rb +31 -0
- data/lib/active_model/gem_version.rb +17 -0
- data/lib/active_model/lint.rb +118 -0
- data/lib/active_model/locale/en.yml +36 -0
- data/lib/active_model/model.rb +99 -0
- data/lib/active_model/naming.rb +318 -0
- data/lib/active_model/railtie.rb +14 -0
- data/lib/active_model/secure_password.rb +129 -0
- data/lib/active_model/serialization.rb +192 -0
- data/lib/active_model/serializers/json.rb +146 -0
- data/lib/active_model/translation.rb +70 -0
- data/lib/active_model/type.rb +53 -0
- data/lib/active_model/type/big_integer.rb +15 -0
- data/lib/active_model/type/binary.rb +52 -0
- data/lib/active_model/type/boolean.rb +38 -0
- data/lib/active_model/type/date.rb +57 -0
- data/lib/active_model/type/date_time.rb +51 -0
- data/lib/active_model/type/decimal.rb +70 -0
- data/lib/active_model/type/float.rb +36 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +41 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +37 -0
- data/lib/active_model/type/helpers/time_value.rb +68 -0
- data/lib/active_model/type/helpers/timezone.rb +19 -0
- data/lib/active_model/type/immutable_string.rb +32 -0
- data/lib/active_model/type/integer.rb +70 -0
- data/lib/active_model/type/registry.rb +70 -0
- data/lib/active_model/type/string.rb +26 -0
- data/lib/active_model/type/time.rb +51 -0
- data/lib/active_model/type/value.rb +126 -0
- data/lib/active_model/validations.rb +439 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +106 -0
- data/lib/active_model/validations/callbacks.rb +122 -0
- data/lib/active_model/validations/clusivity.rb +54 -0
- data/lib/active_model/validations/confirmation.rb +80 -0
- data/lib/active_model/validations/exclusion.rb +49 -0
- data/lib/active_model/validations/format.rb +114 -0
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +47 -0
- data/lib/active_model/validations/length.rb +129 -0
- data/lib/active_model/validations/numericality.rb +189 -0
- data/lib/active_model/validations/presence.rb +39 -0
- data/lib/active_model/validations/validates.rb +174 -0
- data/lib/active_model/validations/with.rb +147 -0
- data/lib/active_model/validator.rb +183 -0
- data/lib/active_model/version.rb +10 -0
- metadata +125 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/array/extract_options"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
# == Active \Model \Callbacks
|
7
|
+
#
|
8
|
+
# Provides an interface for any class to have Active Record like callbacks.
|
9
|
+
#
|
10
|
+
# Like the Active Record methods, the callback chain is aborted as soon as
|
11
|
+
# one of the methods throws +:abort+.
|
12
|
+
#
|
13
|
+
# First, extend ActiveModel::Callbacks from the class you are creating:
|
14
|
+
#
|
15
|
+
# class MyModel
|
16
|
+
# extend ActiveModel::Callbacks
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Then define a list of methods that you want callbacks attached to:
|
20
|
+
#
|
21
|
+
# define_model_callbacks :create, :update
|
22
|
+
#
|
23
|
+
# This will provide all three standard callbacks (before, around and after)
|
24
|
+
# for both the <tt>:create</tt> and <tt>:update</tt> methods. To implement,
|
25
|
+
# you need to wrap the methods you want callbacks on in a block so that the
|
26
|
+
# callbacks get a chance to fire:
|
27
|
+
#
|
28
|
+
# def create
|
29
|
+
# run_callbacks :create do
|
30
|
+
# # Your create action methods here
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Then in your class, you can use the +before_create+, +after_create+ and
|
35
|
+
# +around_create+ methods, just as you would in an Active Record model.
|
36
|
+
#
|
37
|
+
# before_create :action_before_create
|
38
|
+
#
|
39
|
+
# def action_before_create
|
40
|
+
# # Your code here
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# When defining an around callback remember to yield to the block, otherwise
|
44
|
+
# it won't be executed:
|
45
|
+
#
|
46
|
+
# around_create :log_status
|
47
|
+
#
|
48
|
+
# def log_status
|
49
|
+
# puts 'going to call the block...'
|
50
|
+
# yield
|
51
|
+
# puts 'block successfully called.'
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# You can choose to have only specific callbacks by passing a hash to the
|
55
|
+
# +define_model_callbacks+ method.
|
56
|
+
#
|
57
|
+
# define_model_callbacks :create, only: [:after, :before]
|
58
|
+
#
|
59
|
+
# Would only create the +after_create+ and +before_create+ callback methods in
|
60
|
+
# your class.
|
61
|
+
#
|
62
|
+
# NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
|
63
|
+
#
|
64
|
+
module Callbacks
|
65
|
+
def self.extended(base) #:nodoc:
|
66
|
+
base.class_eval do
|
67
|
+
include ActiveSupport::Callbacks
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# define_model_callbacks accepts the same options +define_callbacks+ does,
|
72
|
+
# in case you want to overwrite a default. Besides that, it also accepts an
|
73
|
+
# <tt>:only</tt> option, where you can choose if you want all types (before,
|
74
|
+
# around or after) or just some.
|
75
|
+
#
|
76
|
+
# define_model_callbacks :initializer, only: :after
|
77
|
+
#
|
78
|
+
# Note, the <tt>only: <type></tt> hash will apply to all callbacks defined
|
79
|
+
# on that method call. To get around this you can call the define_model_callbacks
|
80
|
+
# method as many times as you need.
|
81
|
+
#
|
82
|
+
# define_model_callbacks :create, only: :after
|
83
|
+
# define_model_callbacks :update, only: :before
|
84
|
+
# define_model_callbacks :destroy, only: :around
|
85
|
+
#
|
86
|
+
# Would create +after_create+, +before_update+ and +around_destroy+ methods
|
87
|
+
# only.
|
88
|
+
#
|
89
|
+
# You can pass in a class to before_<type>, after_<type> and around_<type>,
|
90
|
+
# in which case the callback will call that class's <action>_<type> method
|
91
|
+
# passing the object that the callback is being called on.
|
92
|
+
#
|
93
|
+
# class MyModel
|
94
|
+
# extend ActiveModel::Callbacks
|
95
|
+
# define_model_callbacks :create
|
96
|
+
#
|
97
|
+
# before_create AnotherClass
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# class AnotherClass
|
101
|
+
# def self.before_create( obj )
|
102
|
+
# # obj is the MyModel instance that the callback is being called on
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# NOTE: +method_name+ passed to define_model_callbacks must not end with
|
107
|
+
# <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
|
108
|
+
def define_model_callbacks(*callbacks)
|
109
|
+
options = callbacks.extract_options!
|
110
|
+
options = {
|
111
|
+
skip_after_callbacks_if_terminated: true,
|
112
|
+
scope: [:kind, :name],
|
113
|
+
only: [:before, :around, :after]
|
114
|
+
}.merge!(options)
|
115
|
+
|
116
|
+
types = Array(options.delete(:only))
|
117
|
+
|
118
|
+
callbacks.each do |callback|
|
119
|
+
define_callbacks(callback, options)
|
120
|
+
|
121
|
+
types.each do |type|
|
122
|
+
send("_define_#{type}_model_callback", self, callback)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def _define_before_model_callback(klass, callback)
|
130
|
+
klass.define_singleton_method("before_#{callback}") do |*args, &block|
|
131
|
+
set_callback(:"#{callback}", :before, *args, &block)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def _define_around_model_callback(klass, callback)
|
136
|
+
klass.define_singleton_method("around_#{callback}") do |*args, &block|
|
137
|
+
set_callback(:"#{callback}", :around, *args, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def _define_after_model_callback(klass, callback)
|
142
|
+
klass.define_singleton_method("after_#{callback}") do |*args, &block|
|
143
|
+
options = args.extract_options!
|
144
|
+
options[:prepend] = true
|
145
|
+
conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
|
146
|
+
v != false
|
147
|
+
}
|
148
|
+
options[:if] = Array(options[:if]) << conditional
|
149
|
+
set_callback(:"#{callback}", :after, *(args << options), &block)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
# == Active \Model \Conversion
|
5
|
+
#
|
6
|
+
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
|
7
|
+
#
|
8
|
+
# Let's take for example this non-persisted object.
|
9
|
+
#
|
10
|
+
# class ContactMessage
|
11
|
+
# include ActiveModel::Conversion
|
12
|
+
#
|
13
|
+
# # ContactMessage are never persisted in the DB
|
14
|
+
# def persisted?
|
15
|
+
# false
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# cm = ContactMessage.new
|
20
|
+
# cm.to_model == cm # => true
|
21
|
+
# cm.to_key # => nil
|
22
|
+
# cm.to_param # => nil
|
23
|
+
# cm.to_partial_path # => "contact_messages/contact_message"
|
24
|
+
module Conversion
|
25
|
+
extend ActiveSupport::Concern
|
26
|
+
|
27
|
+
# If your object is already designed to implement all of the \Active \Model
|
28
|
+
# you can use the default <tt>:to_model</tt> implementation, which simply
|
29
|
+
# returns +self+.
|
30
|
+
#
|
31
|
+
# class Person
|
32
|
+
# include ActiveModel::Conversion
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# person = Person.new
|
36
|
+
# person.to_model == person # => true
|
37
|
+
#
|
38
|
+
# If your model does not act like an \Active \Model object, then you should
|
39
|
+
# define <tt>:to_model</tt> yourself returning a proxy object that wraps
|
40
|
+
# your object with \Active \Model compliant methods.
|
41
|
+
def to_model
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns an Array of all key attributes if any of the attributes is set, whether or not
|
46
|
+
# the object is persisted. Returns +nil+ if there are no key attributes.
|
47
|
+
#
|
48
|
+
# class Person
|
49
|
+
# include ActiveModel::Conversion
|
50
|
+
# attr_accessor :id
|
51
|
+
#
|
52
|
+
# def initialize(id)
|
53
|
+
# @id = id
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# person = Person.new(1)
|
58
|
+
# person.to_key # => [1]
|
59
|
+
def to_key
|
60
|
+
key = respond_to?(:id) && id
|
61
|
+
key ? [key] : nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns a +string+ representing the object's key suitable for use in URLs,
|
65
|
+
# or +nil+ if <tt>persisted?</tt> is +false+.
|
66
|
+
#
|
67
|
+
# class Person
|
68
|
+
# include ActiveModel::Conversion
|
69
|
+
# attr_accessor :id
|
70
|
+
#
|
71
|
+
# def initialize(id)
|
72
|
+
# @id = id
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# def persisted?
|
76
|
+
# true
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# person = Person.new(1)
|
81
|
+
# person.to_param # => "1"
|
82
|
+
def to_param
|
83
|
+
(persisted? && key = to_key) ? key.join("-") : nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns a +string+ identifying the path associated with the object.
|
87
|
+
# ActionPack uses this to find a suitable partial to represent the object.
|
88
|
+
#
|
89
|
+
# class Person
|
90
|
+
# include ActiveModel::Conversion
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# person = Person.new
|
94
|
+
# person.to_partial_path # => "people/person"
|
95
|
+
def to_partial_path
|
96
|
+
self.class._to_partial_path
|
97
|
+
end
|
98
|
+
|
99
|
+
module ClassMethods #:nodoc:
|
100
|
+
# Provide a class level cache for #to_partial_path. This is an
|
101
|
+
# internal method and should not be accessed directly.
|
102
|
+
def _to_partial_path #:nodoc:
|
103
|
+
@_to_partial_path ||= begin
|
104
|
+
element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
|
105
|
+
collection = ActiveSupport::Inflector.tableize(name)
|
106
|
+
"#{collection}/#{element}".freeze
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,343 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/hash_with_indifferent_access"
|
4
|
+
require "active_support/core_ext/object/duplicable"
|
5
|
+
require "active_model/attribute_mutation_tracker"
|
6
|
+
|
7
|
+
module ActiveModel
|
8
|
+
# == Active \Model \Dirty
|
9
|
+
#
|
10
|
+
# Provides a way to track changes in your object in the same way as
|
11
|
+
# Active Record does.
|
12
|
+
#
|
13
|
+
# The requirements for implementing ActiveModel::Dirty are:
|
14
|
+
#
|
15
|
+
# * <tt>include ActiveModel::Dirty</tt> in your object.
|
16
|
+
# * Call <tt>define_attribute_methods</tt> passing each method you want to
|
17
|
+
# track.
|
18
|
+
# * Call <tt>[attr_name]_will_change!</tt> before each change to the tracked
|
19
|
+
# attribute.
|
20
|
+
# * Call <tt>changes_applied</tt> after the changes are persisted.
|
21
|
+
# * Call <tt>clear_changes_information</tt> when you want to reset the changes
|
22
|
+
# information.
|
23
|
+
# * Call <tt>restore_attributes</tt> when you want to restore previous data.
|
24
|
+
#
|
25
|
+
# A minimal implementation could be:
|
26
|
+
#
|
27
|
+
# class Person
|
28
|
+
# include ActiveModel::Dirty
|
29
|
+
#
|
30
|
+
# define_attribute_methods :name
|
31
|
+
#
|
32
|
+
# def initialize
|
33
|
+
# @name = nil
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# def name
|
37
|
+
# @name
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# def name=(val)
|
41
|
+
# name_will_change! unless val == @name
|
42
|
+
# @name = val
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# def save
|
46
|
+
# # do persistence work
|
47
|
+
#
|
48
|
+
# changes_applied
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def reload!
|
52
|
+
# # get the values from the persistence layer
|
53
|
+
#
|
54
|
+
# clear_changes_information
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# def rollback!
|
58
|
+
# restore_attributes
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# A newly instantiated +Person+ object is unchanged:
|
63
|
+
#
|
64
|
+
# person = Person.new
|
65
|
+
# person.changed? # => false
|
66
|
+
#
|
67
|
+
# Change the name:
|
68
|
+
#
|
69
|
+
# person.name = 'Bob'
|
70
|
+
# person.changed? # => true
|
71
|
+
# person.name_changed? # => true
|
72
|
+
# person.name_changed?(from: nil, to: "Bob") # => true
|
73
|
+
# person.name_was # => nil
|
74
|
+
# person.name_change # => [nil, "Bob"]
|
75
|
+
# person.name = 'Bill'
|
76
|
+
# person.name_change # => [nil, "Bill"]
|
77
|
+
#
|
78
|
+
# Save the changes:
|
79
|
+
#
|
80
|
+
# person.save
|
81
|
+
# person.changed? # => false
|
82
|
+
# person.name_changed? # => false
|
83
|
+
#
|
84
|
+
# Reset the changes:
|
85
|
+
#
|
86
|
+
# person.previous_changes # => {"name" => [nil, "Bill"]}
|
87
|
+
# person.name_previously_changed? # => true
|
88
|
+
# person.name_previous_change # => [nil, "Bill"]
|
89
|
+
# person.reload!
|
90
|
+
# person.previous_changes # => {}
|
91
|
+
#
|
92
|
+
# Rollback the changes:
|
93
|
+
#
|
94
|
+
# person.name = "Uncle Bob"
|
95
|
+
# person.rollback!
|
96
|
+
# person.name # => "Bill"
|
97
|
+
# person.name_changed? # => false
|
98
|
+
#
|
99
|
+
# Assigning the same value leaves the attribute unchanged:
|
100
|
+
#
|
101
|
+
# person.name = 'Bill'
|
102
|
+
# person.name_changed? # => false
|
103
|
+
# person.name_change # => nil
|
104
|
+
#
|
105
|
+
# Which attributes have changed?
|
106
|
+
#
|
107
|
+
# person.name = 'Bob'
|
108
|
+
# person.changed # => ["name"]
|
109
|
+
# person.changes # => {"name" => ["Bill", "Bob"]}
|
110
|
+
#
|
111
|
+
# If an attribute is modified in-place then make use of
|
112
|
+
# <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing.
|
113
|
+
# Otherwise \Active \Model can't track changes to in-place attributes. Note
|
114
|
+
# that Active Record can detect in-place modifications automatically. You do
|
115
|
+
# not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models.
|
116
|
+
#
|
117
|
+
# person.name_will_change!
|
118
|
+
# person.name_change # => ["Bill", "Bill"]
|
119
|
+
# person.name << 'y'
|
120
|
+
# person.name_change # => ["Bill", "Billy"]
|
121
|
+
module Dirty
|
122
|
+
extend ActiveSupport::Concern
|
123
|
+
include ActiveModel::AttributeMethods
|
124
|
+
|
125
|
+
OPTION_NOT_GIVEN = Object.new # :nodoc:
|
126
|
+
private_constant :OPTION_NOT_GIVEN
|
127
|
+
|
128
|
+
included do
|
129
|
+
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
|
130
|
+
attribute_method_suffix "_previously_changed?", "_previous_change"
|
131
|
+
attribute_method_affix prefix: "restore_", suffix: "!"
|
132
|
+
end
|
133
|
+
|
134
|
+
def initialize_dup(other) # :nodoc:
|
135
|
+
super
|
136
|
+
if self.class.respond_to?(:_default_attributes)
|
137
|
+
@attributes = self.class._default_attributes.map do |attr|
|
138
|
+
attr.with_value_from_user(@attributes.fetch_value(attr.name))
|
139
|
+
end
|
140
|
+
end
|
141
|
+
@mutations_from_database = nil
|
142
|
+
end
|
143
|
+
|
144
|
+
# Clears dirty data and moves +changes+ to +previously_changed+ and
|
145
|
+
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
|
146
|
+
def changes_applied
|
147
|
+
unless defined?(@attributes)
|
148
|
+
@previously_changed = changes
|
149
|
+
end
|
150
|
+
@mutations_before_last_save = mutations_from_database
|
151
|
+
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
152
|
+
forget_attribute_assignments
|
153
|
+
@mutations_from_database = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
|
157
|
+
#
|
158
|
+
# person.changed? # => false
|
159
|
+
# person.name = 'bob'
|
160
|
+
# person.changed? # => true
|
161
|
+
def changed?
|
162
|
+
changed_attributes.present?
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns an array with the name of the attributes with unsaved changes.
|
166
|
+
#
|
167
|
+
# person.changed # => []
|
168
|
+
# person.name = 'bob'
|
169
|
+
# person.changed # => ["name"]
|
170
|
+
def changed
|
171
|
+
changed_attributes.keys
|
172
|
+
end
|
173
|
+
|
174
|
+
# Handles <tt>*_changed?</tt> for +method_missing+.
|
175
|
+
def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
|
176
|
+
!!changes_include?(attr) &&
|
177
|
+
(to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
|
178
|
+
(from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
|
179
|
+
end
|
180
|
+
|
181
|
+
# Handles <tt>*_was</tt> for +method_missing+.
|
182
|
+
def attribute_was(attr) # :nodoc:
|
183
|
+
attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
|
187
|
+
def attribute_previously_changed?(attr) #:nodoc:
|
188
|
+
previous_changes_include?(attr)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Restore all previous data of the provided attributes.
|
192
|
+
def restore_attributes(attributes = changed)
|
193
|
+
attributes.each { |attr| restore_attribute! attr }
|
194
|
+
end
|
195
|
+
|
196
|
+
# Clears all dirty data: current changes and previous changes.
|
197
|
+
def clear_changes_information
|
198
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
199
|
+
@mutations_before_last_save = nil
|
200
|
+
@attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
|
201
|
+
forget_attribute_assignments
|
202
|
+
@mutations_from_database = nil
|
203
|
+
end
|
204
|
+
|
205
|
+
def clear_attribute_changes(attr_names)
|
206
|
+
attributes_changed_by_setter.except!(*attr_names)
|
207
|
+
attr_names.each do |attr_name|
|
208
|
+
clear_attribute_change(attr_name)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns a hash of the attributes with unsaved changes indicating their original
|
213
|
+
# values like <tt>attr => original value</tt>.
|
214
|
+
#
|
215
|
+
# person.name # => "bob"
|
216
|
+
# person.name = 'robert'
|
217
|
+
# person.changed_attributes # => {"name" => "bob"}
|
218
|
+
def changed_attributes
|
219
|
+
# This should only be set by methods which will call changed_attributes
|
220
|
+
# multiple times when it is known that the computed value cannot change.
|
221
|
+
if defined?(@cached_changed_attributes)
|
222
|
+
@cached_changed_attributes
|
223
|
+
else
|
224
|
+
attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Returns a hash of changed attributes indicating their original
|
229
|
+
# and new values like <tt>attr => [original value, new value]</tt>.
|
230
|
+
#
|
231
|
+
# person.changes # => {}
|
232
|
+
# person.name = 'bob'
|
233
|
+
# person.changes # => { "name" => ["bill", "bob"] }
|
234
|
+
def changes
|
235
|
+
cache_changed_attributes do
|
236
|
+
ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns a hash of attributes that were changed before the model was saved.
|
241
|
+
#
|
242
|
+
# person.name # => "bob"
|
243
|
+
# person.name = 'robert'
|
244
|
+
# person.save
|
245
|
+
# person.previous_changes # => {"name" => ["bob", "robert"]}
|
246
|
+
def previous_changes
|
247
|
+
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
|
248
|
+
@previously_changed.merge(mutations_before_last_save.changes)
|
249
|
+
end
|
250
|
+
|
251
|
+
def attribute_changed_in_place?(attr_name) # :nodoc:
|
252
|
+
mutations_from_database.changed_in_place?(attr_name)
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
def clear_attribute_change(attr_name)
|
257
|
+
mutations_from_database.forget_change(attr_name)
|
258
|
+
end
|
259
|
+
|
260
|
+
def mutations_from_database
|
261
|
+
unless defined?(@mutations_from_database)
|
262
|
+
@mutations_from_database = nil
|
263
|
+
end
|
264
|
+
@mutations_from_database ||= if defined?(@attributes)
|
265
|
+
ActiveModel::AttributeMutationTracker.new(@attributes)
|
266
|
+
else
|
267
|
+
NullMutationTracker.instance
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def forget_attribute_assignments
|
272
|
+
@attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
|
273
|
+
end
|
274
|
+
|
275
|
+
def mutations_before_last_save
|
276
|
+
@mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
|
277
|
+
end
|
278
|
+
|
279
|
+
def cache_changed_attributes
|
280
|
+
@cached_changed_attributes = changed_attributes
|
281
|
+
yield
|
282
|
+
ensure
|
283
|
+
clear_changed_attributes_cache
|
284
|
+
end
|
285
|
+
|
286
|
+
def clear_changed_attributes_cache
|
287
|
+
remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Returns +true+ if attr_name is changed, +false+ otherwise.
|
291
|
+
def changes_include?(attr_name)
|
292
|
+
attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name)
|
293
|
+
end
|
294
|
+
alias attribute_changed_by_setter? changes_include?
|
295
|
+
|
296
|
+
# Returns +true+ if attr_name were changed before the model was saved,
|
297
|
+
# +false+ otherwise.
|
298
|
+
def previous_changes_include?(attr_name)
|
299
|
+
previous_changes.include?(attr_name)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Handles <tt>*_change</tt> for +method_missing+.
|
303
|
+
def attribute_change(attr)
|
304
|
+
[changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr)
|
305
|
+
end
|
306
|
+
|
307
|
+
# Handles <tt>*_previous_change</tt> for +method_missing+.
|
308
|
+
def attribute_previous_change(attr)
|
309
|
+
previous_changes[attr] if attribute_previously_changed?(attr)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Handles <tt>*_will_change!</tt> for +method_missing+.
|
313
|
+
def attribute_will_change!(attr)
|
314
|
+
unless attribute_changed?(attr)
|
315
|
+
begin
|
316
|
+
value = _read_attribute(attr)
|
317
|
+
value = value.duplicable? ? value.clone : value
|
318
|
+
rescue TypeError, NoMethodError
|
319
|
+
end
|
320
|
+
|
321
|
+
set_attribute_was(attr, value)
|
322
|
+
end
|
323
|
+
mutations_from_database.force_change(attr)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Handles <tt>restore_*!</tt> for +method_missing+.
|
327
|
+
def restore_attribute!(attr)
|
328
|
+
if attribute_changed?(attr)
|
329
|
+
__send__("#{attr}=", changed_attributes[attr])
|
330
|
+
clear_attribute_changes([attr])
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def attributes_changed_by_setter
|
335
|
+
@attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
|
336
|
+
end
|
337
|
+
|
338
|
+
# Force an attribute to have a particular "before" value
|
339
|
+
def set_attribute_was(attr, old_value)
|
340
|
+
attributes_changed_by_setter[attr] = old_value
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|