activemodel 4.2.0 → 6.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +49 -37
- data/MIT-LICENSE +1 -1
- data/README.rdoc +16 -22
- data/lib/active_model/attribute/user_provided_default.rb +51 -0
- data/lib/active_model/attribute.rb +248 -0
- data/lib/active_model/attribute_assignment.rb +55 -0
- data/lib/active_model/attribute_methods.rb +150 -73
- data/lib/active_model/attribute_mutation_tracker.rb +181 -0
- data/lib/active_model/attribute_set/builder.rb +191 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +106 -0
- data/lib/active_model/attributes.rb +132 -0
- data/lib/active_model/callbacks.rb +31 -25
- data/lib/active_model/conversion.rb +20 -9
- data/lib/active_model/dirty.rb +142 -116
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +436 -202
- data/lib/active_model/forbidden_attributes_protection.rb +6 -3
- data/lib/active_model/gem_version.rb +5 -3
- data/lib/active_model/lint.rb +47 -42
- data/lib/active_model/locale/en.yml +2 -1
- data/lib/active_model/model.rb +7 -7
- data/lib/active_model/naming.rb +36 -18
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +8 -0
- data/lib/active_model/secure_password.rb +61 -67
- data/lib/active_model/serialization.rb +48 -17
- data/lib/active_model/serializers/json.rb +22 -13
- data/lib/active_model/translation.rb +5 -4
- data/lib/active_model/type/big_integer.rb +14 -0
- data/lib/active_model/type/binary.rb +52 -0
- data/lib/active_model/type/boolean.rb +46 -0
- data/lib/active_model/type/date.rb +52 -0
- data/lib/active_model/type/date_time.rb +46 -0
- data/lib/active_model/type/decimal.rb +69 -0
- data/lib/active_model/type/float.rb +35 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +48 -0
- data/lib/active_model/type/helpers/time_value.rb +90 -0
- data/lib/active_model/type/helpers/timezone.rb +19 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +35 -0
- data/lib/active_model/type/integer.rb +67 -0
- data/lib/active_model/type/registry.rb +70 -0
- data/lib/active_model/type/string.rb +35 -0
- data/lib/active_model/type/time.rb +46 -0
- data/lib/active_model/type/value.rb +133 -0
- data/lib/active_model/type.rb +53 -0
- data/lib/active_model/validations/absence.rb +6 -4
- data/lib/active_model/validations/acceptance.rb +72 -14
- data/lib/active_model/validations/callbacks.rb +23 -19
- data/lib/active_model/validations/clusivity.rb +18 -12
- data/lib/active_model/validations/confirmation.rb +27 -14
- data/lib/active_model/validations/exclusion.rb +7 -4
- data/lib/active_model/validations/format.rb +27 -27
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +8 -7
- data/lib/active_model/validations/length.rb +35 -32
- data/lib/active_model/validations/numericality.rb +72 -34
- data/lib/active_model/validations/presence.rb +3 -3
- data/lib/active_model/validations/validates.rb +17 -15
- data/lib/active_model/validations/with.rb +6 -12
- data/lib/active_model/validations.rb +58 -23
- data/lib/active_model/validator.rb +23 -17
- data/lib/active_model/version.rb +4 -2
- data/lib/active_model.rb +18 -11
- metadata +44 -25
- data/lib/active_model/serializers/xml.rb +0 -238
- data/lib/active_model/test_case.rb +0 -4
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
# Raised when forbidden attributes are used for mass assignment.
|
3
5
|
#
|
@@ -15,10 +17,11 @@ module ActiveModel
|
|
15
17
|
end
|
16
18
|
|
17
19
|
module ForbiddenAttributesProtection # :nodoc:
|
18
|
-
|
20
|
+
private
|
19
21
|
def sanitize_for_mass_assignment(attributes)
|
20
|
-
if attributes.respond_to?(:permitted?)
|
21
|
-
raise ActiveModel::ForbiddenAttributesError
|
22
|
+
if attributes.respond_to?(:permitted?)
|
23
|
+
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
|
24
|
+
attributes.to_h
|
22
25
|
else
|
23
26
|
attributes
|
24
27
|
end
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
|
-
# Returns the version of the currently loaded Active Model as a <tt>Gem::Version</tt>
|
4
|
+
# Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
|
3
5
|
def self.gem_version
|
4
6
|
Gem::Version.new VERSION::STRING
|
5
7
|
end
|
6
8
|
|
7
9
|
module VERSION
|
8
|
-
MAJOR =
|
9
|
-
MINOR =
|
10
|
+
MAJOR = 6
|
11
|
+
MINOR = 1
|
10
12
|
TINY = 0
|
11
13
|
PRE = nil
|
12
14
|
|
data/lib/active_model/lint.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
module Lint
|
3
5
|
# == Active \Model \Lint \Tests
|
@@ -20,88 +22,91 @@ module ActiveModel
|
|
20
22
|
# to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return
|
21
23
|
# +self+.
|
22
24
|
module Tests
|
23
|
-
|
24
|
-
#
|
25
|
+
# Passes if the object's model responds to <tt>to_key</tt> and if calling
|
26
|
+
# this method returns +nil+ when the object is not persisted.
|
27
|
+
# Fails otherwise.
|
25
28
|
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# <tt>dom_id</tt> to generate unique ids for the object.
|
29
|
+
# <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
|
30
|
+
# of the model, and is used to a generate unique DOM id for the object.
|
29
31
|
def test_to_key
|
30
|
-
|
32
|
+
assert_respond_to model, :to_key
|
31
33
|
def model.persisted?() false end
|
32
34
|
assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
|
33
35
|
end
|
34
36
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# or +nil+ if <tt>model.persisted?</tt> is +false+.
|
37
|
+
# Passes if the object's model responds to <tt>to_param</tt> and if
|
38
|
+
# calling this method returns +nil+ when the object is not persisted.
|
39
|
+
# Fails otherwise.
|
39
40
|
#
|
41
|
+
# <tt>to_param</tt> is used to represent the object's key in URLs.
|
40
42
|
# Implementers can decide to either raise an exception or provide a
|
41
43
|
# default in case the record uses a composite primary key. There are no
|
42
44
|
# tests for this behavior in lint because it doesn't make sense to force
|
43
45
|
# any of the possible implementation strategies on the implementer.
|
44
|
-
# However, if the resource is not persisted?, then <tt>to_param</tt>
|
45
|
-
# should always return +nil+.
|
46
46
|
def test_to_param
|
47
|
-
|
47
|
+
assert_respond_to model, :to_param
|
48
48
|
def model.to_key() [1] end
|
49
49
|
def model.persisted?() false end
|
50
50
|
assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
|
51
51
|
end
|
52
52
|
|
53
|
-
#
|
53
|
+
# Passes if the object's model responds to <tt>to_partial_path</tt> and if
|
54
|
+
# calling this method returns a string. Fails otherwise.
|
54
55
|
#
|
55
|
-
#
|
56
|
-
#
|
56
|
+
# <tt>to_partial_path</tt> is used for looking up partials. For example,
|
57
|
+
# a BlogPost model might return "blog_posts/blog_post".
|
57
58
|
def test_to_partial_path
|
58
|
-
|
59
|
+
assert_respond_to model, :to_partial_path
|
59
60
|
assert_kind_of String, model.to_partial_path
|
60
61
|
end
|
61
62
|
|
62
|
-
#
|
63
|
+
# Passes if the object's model responds to <tt>persisted?</tt> and if
|
64
|
+
# calling this method returns either +true+ or +false+. Fails otherwise.
|
63
65
|
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# to the update action.
|
66
|
+
# <tt>persisted?</tt> is used when calculating the URL for an object.
|
67
|
+
# If the object is not persisted, a form for that object, for instance,
|
68
|
+
# will route to the create action. If it is persisted, a form for the
|
69
|
+
# object will route to the update action.
|
69
70
|
def test_persisted?
|
70
|
-
|
71
|
+
assert_respond_to model, :persisted?
|
71
72
|
assert_boolean model.persisted?, "persisted?"
|
72
73
|
end
|
73
74
|
|
74
|
-
#
|
75
|
+
# Passes if the object's model responds to <tt>model_name</tt> both as
|
76
|
+
# an instance method and as a class method, and if calling this method
|
77
|
+
# returns a string with some convenience methods: <tt>:human</tt>,
|
78
|
+
# <tt>:singular</tt> and <tt>:plural</tt>.
|
75
79
|
#
|
76
|
-
#
|
77
|
-
# convenience methods: # <tt>:human</tt>, <tt>:singular</tt> and
|
78
|
-
# <tt>:plural</tt>. Check ActiveModel::Naming for more information.
|
80
|
+
# Check ActiveModel::Naming for more information.
|
79
81
|
def test_model_naming
|
80
|
-
|
82
|
+
assert_respond_to model.class, :model_name
|
81
83
|
model_name = model.class.model_name
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
assert_respond_to model_name, :to_str
|
85
|
+
assert_respond_to model_name.human, :to_str
|
86
|
+
assert_respond_to model_name.singular, :to_str
|
87
|
+
assert_respond_to model_name.plural, :to_str
|
86
88
|
|
87
|
-
|
89
|
+
assert_respond_to model, :model_name
|
88
90
|
assert_equal model.model_name, model.class.model_name
|
89
91
|
end
|
90
92
|
|
91
|
-
#
|
93
|
+
# Passes if the object's model responds to <tt>errors</tt> and if calling
|
94
|
+
# <tt>[](attribute)</tt> on the result of this method returns an array.
|
95
|
+
# Fails otherwise.
|
92
96
|
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
+
# <tt>errors[attribute]</tt> is used to retrieve the errors of a model
|
98
|
+
# for a given attribute. If errors are present, the method should return
|
99
|
+
# an array of strings that are the errors for the attribute in question.
|
100
|
+
# If localization is used, the strings should be localized for the current
|
101
|
+
# locale. If no error is present, the method should return an empty array.
|
97
102
|
def test_errors_aref
|
98
|
-
|
99
|
-
|
103
|
+
assert_respond_to model, :errors
|
104
|
+
assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
|
100
105
|
end
|
101
106
|
|
102
107
|
private
|
103
108
|
def model
|
104
|
-
|
109
|
+
assert_respond_to @model, :to_model
|
105
110
|
@model.to_model
|
106
111
|
end
|
107
112
|
|
@@ -6,6 +6,7 @@ en:
|
|
6
6
|
# The values :model, :attribute and :value are always available for interpolation
|
7
7
|
# The value :count is available when applicable. Can be used for pluralization.
|
8
8
|
messages:
|
9
|
+
model_invalid: "Validation failed: %{errors}"
|
9
10
|
inclusion: "is not included in the list"
|
10
11
|
exclusion: "is reserved"
|
11
12
|
invalid: "is invalid"
|
@@ -16,7 +17,7 @@ en:
|
|
16
17
|
present: "must be blank"
|
17
18
|
too_long:
|
18
19
|
one: "is too long (maximum is 1 character)"
|
19
|
-
other: "is too long (maximum is %{count} characters)"
|
20
|
+
other: "is too long (maximum is %{count} characters)"
|
20
21
|
too_short:
|
21
22
|
one: "is too short (minimum is 1 character)"
|
22
23
|
other: "is too short (minimum is %{count} characters)"
|
data/lib/active_model/model.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module ActiveModel
|
3
4
|
# == Active \Model \Basic \Model
|
4
5
|
#
|
5
6
|
# Includes the required interface for an object to interact with
|
6
|
-
#
|
7
|
+
# Action Pack and Action View, using different Active Model modules.
|
7
8
|
# It includes model name introspections, conversions, translations and
|
8
9
|
# validations. Besides that, it allows you to initialize the object with a
|
9
|
-
# hash of attributes, pretty much like
|
10
|
+
# hash of attributes, pretty much like Active Record does.
|
10
11
|
#
|
11
12
|
# A minimal implementation could be:
|
12
13
|
#
|
@@ -57,6 +58,7 @@ module ActiveModel
|
|
57
58
|
# (see below).
|
58
59
|
module Model
|
59
60
|
extend ActiveSupport::Concern
|
61
|
+
include ActiveModel::AttributeAssignment
|
60
62
|
include ActiveModel::Validations
|
61
63
|
include ActiveModel::Conversion
|
62
64
|
|
@@ -75,10 +77,8 @@ module ActiveModel
|
|
75
77
|
# person = Person.new(name: 'bob', age: '18')
|
76
78
|
# person.name # => "bob"
|
77
79
|
# person.age # => "18"
|
78
|
-
def initialize(
|
79
|
-
|
80
|
-
self.public_send("#{attr}=", value)
|
81
|
-
end if params
|
80
|
+
def initialize(attributes = {})
|
81
|
+
assign_attributes(attributes) if attributes
|
82
82
|
|
83
83
|
super()
|
84
84
|
end
|
data/lib/active_model/naming.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
require "active_support/core_ext/module/introspection"
|
5
|
+
require "active_support/core_ext/module/redefine_method"
|
3
6
|
|
4
7
|
module ActiveModel
|
5
8
|
class Name
|
6
9
|
include Comparable
|
7
10
|
|
8
|
-
|
11
|
+
attr_accessor :singular, :plural, :element, :collection,
|
9
12
|
:singular_route_key, :route_key, :param_key, :i18n_key,
|
10
13
|
:name
|
11
14
|
|
@@ -46,7 +49,7 @@ module ActiveModel
|
|
46
49
|
# :method: <=>
|
47
50
|
#
|
48
51
|
# :call-seq:
|
49
|
-
#
|
52
|
+
# <=>(other)
|
50
53
|
#
|
51
54
|
# Equivalent to <tt>String#<=></tt>.
|
52
55
|
#
|
@@ -107,6 +110,22 @@ module ActiveModel
|
|
107
110
|
# BlogPost.model_name.eql?('BlogPost') # => true
|
108
111
|
# BlogPost.model_name.eql?('Blog Post') # => false
|
109
112
|
|
113
|
+
##
|
114
|
+
# :method: match?
|
115
|
+
#
|
116
|
+
# :call-seq:
|
117
|
+
# match?(regexp)
|
118
|
+
#
|
119
|
+
# Equivalent to <tt>String#match?</tt>. Match the class name against the
|
120
|
+
# given regexp. Returns +true+ if there is a match, otherwise +false+.
|
121
|
+
#
|
122
|
+
# class BlogPost
|
123
|
+
# extend ActiveModel::Naming
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# BlogPost.model_name.match?(/Post/) # => true
|
127
|
+
# BlogPost.model_name.match?(/\d/) # => false
|
128
|
+
|
110
129
|
##
|
111
130
|
# :method: to_s
|
112
131
|
#
|
@@ -128,8 +147,8 @@ module ActiveModel
|
|
128
147
|
# to_str()
|
129
148
|
#
|
130
149
|
# Equivalent to +to_s+.
|
131
|
-
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
|
132
|
-
:to_str, to: :name
|
150
|
+
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
|
151
|
+
:to_str, :as_json, to: :name
|
133
152
|
|
134
153
|
# Returns a new ActiveModel::Name instance. By default, the +namespace+
|
135
154
|
# and +name+ option will take the namespace and name of the given class
|
@@ -147,7 +166,7 @@ module ActiveModel
|
|
147
166
|
|
148
167
|
raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
|
149
168
|
|
150
|
-
@unnamespaced = @name.
|
169
|
+
@unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
|
151
170
|
@klass = klass
|
152
171
|
@singular = _singularize(@name)
|
153
172
|
@plural = ActiveSupport::Inflector.pluralize(@singular)
|
@@ -162,7 +181,7 @@ module ActiveModel
|
|
162
181
|
@route_key << "_index" if @plural == @singular
|
163
182
|
end
|
164
183
|
|
165
|
-
# Transform the model name into a more
|
184
|
+
# Transform the model name into a more human format, using I18n. By default,
|
166
185
|
# it will underscore then humanize the class name.
|
167
186
|
#
|
168
187
|
# class BlogPost
|
@@ -172,7 +191,7 @@ module ActiveModel
|
|
172
191
|
# BlogPost.model_name.human # => "Blog post"
|
173
192
|
#
|
174
193
|
# Specify +options+ with additional translating options.
|
175
|
-
def human(options={})
|
194
|
+
def human(options = {})
|
176
195
|
return @human unless @klass.respond_to?(:lookup_ancestors) &&
|
177
196
|
@klass.respond_to?(:i18n_scope)
|
178
197
|
|
@@ -184,14 +203,13 @@ module ActiveModel
|
|
184
203
|
defaults << @human
|
185
204
|
|
186
205
|
options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
|
187
|
-
I18n.translate(defaults.shift, options)
|
206
|
+
I18n.translate(defaults.shift, **options)
|
188
207
|
end
|
189
208
|
|
190
209
|
private
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
end
|
210
|
+
def _singularize(string)
|
211
|
+
ActiveSupport::Inflector.underscore(string).tr("/", "_")
|
212
|
+
end
|
195
213
|
end
|
196
214
|
|
197
215
|
# == Active \Model \Naming
|
@@ -211,11 +229,11 @@ module ActiveModel
|
|
211
229
|
# BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
|
212
230
|
#
|
213
231
|
# Providing the functionality that ActiveModel::Naming provides in your object
|
214
|
-
# is required to pass the Active Model Lint test. So either extending the
|
232
|
+
# is required to pass the \Active \Model Lint test. So either extending the
|
215
233
|
# provided method below, or rolling your own is required.
|
216
234
|
module Naming
|
217
235
|
def self.extended(base) #:nodoc:
|
218
|
-
base.
|
236
|
+
base.silence_redefinition_of_method :model_name
|
219
237
|
base.delegate :model_name, to: :class
|
220
238
|
end
|
221
239
|
|
@@ -224,7 +242,7 @@ module ActiveModel
|
|
224
242
|
# (See ActiveModel::Name for more information).
|
225
243
|
#
|
226
244
|
# class Person
|
227
|
-
#
|
245
|
+
# extend ActiveModel::Naming
|
228
246
|
# end
|
229
247
|
#
|
230
248
|
# Person.model_name.name # => "Person"
|
@@ -233,7 +251,7 @@ module ActiveModel
|
|
233
251
|
# Person.model_name.plural # => "people"
|
234
252
|
def model_name
|
235
253
|
@_model_name ||= begin
|
236
|
-
namespace =
|
254
|
+
namespace = module_parents.detect do |n|
|
237
255
|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
238
256
|
end
|
239
257
|
ActiveModel::Name.new(self, namespace)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/error"
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module ActiveModel
|
7
|
+
class NestedError < Error
|
8
|
+
def initialize(base, inner_error, override_options = {})
|
9
|
+
@base = base
|
10
|
+
@inner_error = inner_error
|
11
|
+
@attribute = override_options.fetch(:attribute) { inner_error.attribute }
|
12
|
+
@type = override_options.fetch(:type) { inner_error.type }
|
13
|
+
@raw_type = inner_error.raw_type
|
14
|
+
@options = inner_error.options
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :inner_error
|
18
|
+
|
19
|
+
extend Forwardable
|
20
|
+
def_delegators :@inner_error, :message
|
21
|
+
end
|
22
|
+
end
|
data/lib/active_model/railtie.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_model"
|
2
4
|
require "rails"
|
3
5
|
|
@@ -5,8 +7,14 @@ module ActiveModel
|
|
5
7
|
class Railtie < Rails::Railtie # :nodoc:
|
6
8
|
config.eager_load_namespaces << ActiveModel
|
7
9
|
|
10
|
+
config.active_model = ActiveSupport::OrderedOptions.new
|
11
|
+
|
8
12
|
initializer "active_model.secure_password" do
|
9
13
|
ActiveModel::SecurePassword.min_cost = Rails.env.test?
|
10
14
|
end
|
15
|
+
|
16
|
+
initializer "active_model.i18n_customize_full_message" do
|
17
|
+
ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
|
18
|
+
end
|
11
19
|
end
|
12
20
|
end
|
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveModel
|
2
4
|
module SecurePassword
|
3
5
|
extend ActiveSupport::Concern
|
4
6
|
|
5
|
-
# BCrypt hash function can handle maximum 72
|
6
|
-
# password of length more than 72
|
7
|
+
# BCrypt hash function can handle maximum 72 bytes, and if we pass
|
8
|
+
# password of length more than 72 bytes it ignores extra characters.
|
7
9
|
# Hence need to put a restriction on password length.
|
8
10
|
MAX_PASSWORD_LENGTH_ALLOWED = 72
|
9
11
|
|
@@ -14,19 +16,20 @@ module ActiveModel
|
|
14
16
|
|
15
17
|
module ClassMethods
|
16
18
|
# Adds methods to set and authenticate against a BCrypt password.
|
17
|
-
# This mechanism requires you to have a +
|
19
|
+
# This mechanism requires you to have a +XXX_digest+ attribute.
|
20
|
+
# Where +XXX+ is the attribute name of your desired password.
|
18
21
|
#
|
19
22
|
# The following validations are added automatically:
|
20
23
|
# * Password must be present on creation
|
21
|
-
# * Password length should be less than or equal to 72
|
22
|
-
# * Confirmation of password (using a +
|
24
|
+
# * Password length should be less than or equal to 72 bytes
|
25
|
+
# * Confirmation of password (using a +XXX_confirmation+ attribute)
|
23
26
|
#
|
24
|
-
# If
|
25
|
-
# value for +
|
27
|
+
# If confirmation validation is not needed, simply leave out the
|
28
|
+
# value for +XXX_confirmation+ (i.e. don't provide a form field for
|
26
29
|
# it). When this attribute has a +nil+ value, the validation will not be
|
27
30
|
# triggered.
|
28
31
|
#
|
29
|
-
# For further customizability, it is possible to
|
32
|
+
# For further customizability, it is possible to suppress the default
|
30
33
|
# validations by passing <tt>validations: false</tt> as an argument.
|
31
34
|
#
|
32
35
|
# Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
|
@@ -35,35 +38,40 @@ module ActiveModel
|
|
35
38
|
#
|
36
39
|
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
|
37
40
|
#
|
38
|
-
# # Schema: User(name:string, password_digest:string)
|
41
|
+
# # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
|
39
42
|
# class User < ActiveRecord::Base
|
40
43
|
# has_secure_password
|
44
|
+
# has_secure_password :recovery_password, validations: false
|
41
45
|
# end
|
42
46
|
#
|
43
47
|
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
|
44
|
-
# user.save
|
48
|
+
# user.save # => false, password required
|
45
49
|
# user.password = 'mUc3m00RsqyRe'
|
46
|
-
# user.save
|
50
|
+
# user.save # => false, confirmation doesn't match
|
47
51
|
# user.password_confirmation = 'mUc3m00RsqyRe'
|
48
|
-
# user.save
|
49
|
-
# user.
|
50
|
-
# user.
|
51
|
-
#
|
52
|
-
#
|
53
|
-
|
52
|
+
# user.save # => true
|
53
|
+
# user.recovery_password = "42password"
|
54
|
+
# user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
|
55
|
+
# user.save # => true
|
56
|
+
# user.authenticate('notright') # => false
|
57
|
+
# user.authenticate('mUc3m00RsqyRe') # => user
|
58
|
+
# user.authenticate_recovery_password('42password') # => user
|
59
|
+
# User.find_by(name: 'david')&.authenticate('notright') # => false
|
60
|
+
# User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
|
61
|
+
def has_secure_password(attribute = :password, validations: true)
|
54
62
|
# Load bcrypt gem only when has_secure_password is used.
|
55
63
|
# This is to avoid ActiveModel (and by extension the entire framework)
|
56
64
|
# being dependent on a binary library.
|
57
65
|
begin
|
58
|
-
require
|
66
|
+
require "bcrypt"
|
59
67
|
rescue LoadError
|
60
68
|
$stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
|
61
69
|
raise
|
62
70
|
end
|
63
71
|
|
64
|
-
include InstanceMethodsOnActivation
|
72
|
+
include InstanceMethodsOnActivation.new(attribute)
|
65
73
|
|
66
|
-
if
|
74
|
+
if validations
|
67
75
|
include ActiveModel::Validations
|
68
76
|
|
69
77
|
# This ensures the model has a password by checking whether the password_digest
|
@@ -71,63 +79,49 @@ module ActiveModel
|
|
71
79
|
# when there is an error, the message is added to the password attribute instead
|
72
80
|
# so that the error message will make sense to the end-user.
|
73
81
|
validate do |record|
|
74
|
-
record.errors.add(
|
82
|
+
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
|
75
83
|
end
|
76
84
|
|
77
|
-
validates_length_of
|
78
|
-
validates_confirmation_of
|
79
|
-
end
|
80
|
-
|
81
|
-
# This code is necessary as long as the protected_attributes gem is supported.
|
82
|
-
if respond_to?(:attributes_protected_by_default)
|
83
|
-
def self.attributes_protected_by_default #:nodoc:
|
84
|
-
super + ['password_digest']
|
85
|
-
end
|
85
|
+
validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
|
86
|
+
validates_confirmation_of attribute, allow_blank: true
|
86
87
|
end
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# class User < ActiveRecord::Base
|
94
|
-
# has_secure_password validations: false
|
95
|
-
# end
|
96
|
-
#
|
97
|
-
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
98
|
-
# user.save
|
99
|
-
# user.authenticate('notright') # => false
|
100
|
-
# user.authenticate('mUc3m00RsqyRe') # => user
|
101
|
-
def authenticate(unencrypted_password)
|
102
|
-
BCrypt::Password.new(password_digest) == unencrypted_password && self
|
103
|
-
end
|
91
|
+
class InstanceMethodsOnActivation < Module
|
92
|
+
def initialize(attribute)
|
93
|
+
attr_reader attribute
|
104
94
|
|
105
|
-
|
95
|
+
define_method("#{attribute}=") do |unencrypted_password|
|
96
|
+
if unencrypted_password.nil?
|
97
|
+
self.public_send("#{attribute}_digest=", nil)
|
98
|
+
elsif !unencrypted_password.empty?
|
99
|
+
instance_variable_set("@#{attribute}", unencrypted_password)
|
100
|
+
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
101
|
+
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
|
102
|
+
end
|
103
|
+
end
|
106
104
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
|
125
|
-
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
|
105
|
+
define_method("#{attribute}_confirmation=") do |unencrypted_password|
|
106
|
+
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns +self+ if the password is correct, otherwise +false+.
|
110
|
+
#
|
111
|
+
# class User < ActiveRecord::Base
|
112
|
+
# has_secure_password validations: false
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
|
116
|
+
# user.save
|
117
|
+
# user.authenticate_password('notright') # => false
|
118
|
+
# user.authenticate_password('mUc3m00RsqyRe') # => user
|
119
|
+
define_method("authenticate_#{attribute}") do |unencrypted_password|
|
120
|
+
attribute_digest = public_send("#{attribute}_digest")
|
121
|
+
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
|
126
122
|
end
|
127
|
-
end
|
128
123
|
|
129
|
-
|
130
|
-
@password_confirmation = unencrypted_password
|
124
|
+
alias_method :authenticate, :authenticate_password if attribute == :password
|
131
125
|
end
|
132
126
|
end
|
133
127
|
end
|