mongoid 8.0.7 → 8.0.9
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 +4 -4
- data/Rakefile +65 -41
- data/lib/mongoid/association/accessors.rb +5 -1
- data/lib/mongoid/association/eager_loadable.rb +3 -0
- data/lib/mongoid/atomic.rb +9 -7
- data/lib/mongoid/config.rb +10 -0
- data/lib/mongoid/criteria/queryable/extensions/numeric.rb +15 -1
- data/lib/mongoid/document.rb +8 -1
- data/lib/mongoid/fields.rb +11 -6
- data/lib/mongoid/interceptable.rb +10 -8
- data/lib/mongoid/timestamps/created.rb +8 -1
- data/lib/mongoid/traversable.rb +12 -0
- data/lib/mongoid/validatable/associated.rb +98 -17
- data/lib/mongoid/validatable.rb +8 -0
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +40 -0
- data/spec/integration/callbacks_models.rb +37 -0
- data/spec/integration/callbacks_spec.rb +27 -0
- data/spec/mongoid/association/eager_spec.rb +24 -2
- data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +4 -0
- data/spec/mongoid/association_spec.rb +60 -0
- data/spec/mongoid/document_spec.rb +27 -0
- data/spec/mongoid/interceptable_spec.rb +100 -0
- data/spec/mongoid/interceptable_spec_models.rb +51 -111
- data/spec/mongoid/serializable_spec.rb +14 -14
- data/spec/mongoid/timestamps/created_spec.rb +23 -0
- data/spec/mongoid/validatable/associated_spec.rb +27 -34
- data/spec/support/models/name.rb +10 -0
- metadata +4 -80
- checksums.yaml.gz.sig +0 -0
- data/spec/shared/LICENSE +0 -20
- data/spec/shared/bin/get-mongodb-download-url +0 -17
- data/spec/shared/bin/s3-copy +0 -45
- data/spec/shared/bin/s3-upload +0 -69
- data/spec/shared/lib/mrss/child_process_helper.rb +0 -80
- data/spec/shared/lib/mrss/cluster_config.rb +0 -231
- data/spec/shared/lib/mrss/constraints.rb +0 -378
- data/spec/shared/lib/mrss/docker_runner.rb +0 -298
- data/spec/shared/lib/mrss/eg_config_utils.rb +0 -51
- data/spec/shared/lib/mrss/event_subscriber.rb +0 -210
- data/spec/shared/lib/mrss/lite_constraints.rb +0 -238
- data/spec/shared/lib/mrss/server_version_registry.rb +0 -113
- data/spec/shared/lib/mrss/session_registry.rb +0 -69
- data/spec/shared/lib/mrss/session_registry_legacy.rb +0 -60
- data/spec/shared/lib/mrss/spec_organizer.rb +0 -179
- data/spec/shared/lib/mrss/utils.rb +0 -37
- data/spec/shared/share/Dockerfile.erb +0 -321
- data/spec/shared/share/haproxy-1.conf +0 -16
- data/spec/shared/share/haproxy-2.conf +0 -17
- data/spec/shared/shlib/config.sh +0 -27
- data/spec/shared/shlib/distro.sh +0 -74
- data/spec/shared/shlib/server.sh +0 -416
- data/spec/shared/shlib/set_env.sh +0 -169
- data.tar.gz.sig +0 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49662a14c4c7135e3403cab365f7e46928baf2efca9ede4e673269d39c5a7292
|
4
|
+
data.tar.gz: a69c22af01dbba7be588e2b4a9a63a1f58323ddd3395771969652323b2d7ecaf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cdde64cee0c2f085b6f250aae4e0f31340a9b79d04177e69f9c287170d8bb2eaba435873d680f77181587624244c1ba5885eff2d3964f3ebbe5f0924a70b1347
|
7
|
+
data.tar.gz: 2d065d480c992dc3a2e772b7c33a0b50805d981c4f150f06b93f0ad9c89fd6b9c138c64354f8353c1a9849d9ddef45eae382bfc3ca1e4bb6aead94e60c5e6324
|
data/Rakefile
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "bundler"
|
4
|
-
require "bundler/gem_tasks"
|
5
4
|
Bundler.setup
|
6
5
|
|
7
6
|
ROOT = File.expand_path(File.join(File.dirname(__FILE__)))
|
@@ -10,34 +9,53 @@ $: << File.join(ROOT, 'spec/shared/lib')
|
|
10
9
|
|
11
10
|
require "rake"
|
12
11
|
require "rspec/core/rake_task"
|
13
|
-
require 'mrss/spec_organizer'
|
14
|
-
require 'rubygems/package'
|
15
|
-
require 'rubygems/security/policies'
|
16
|
-
|
17
|
-
def signed_gem?(path_to_gem)
|
18
|
-
Gem::Package.new(path_to_gem, Gem::Security::HighSecurity).verify
|
19
|
-
true
|
20
|
-
rescue Gem::Security::Exception => e
|
21
|
-
false
|
22
|
-
end
|
23
|
-
|
24
|
-
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
25
|
-
require "mongoid/version"
|
26
|
-
|
27
|
-
tasks = Rake.application.instance_variable_get('@tasks')
|
28
|
-
tasks['release:do'] = tasks.delete('release')
|
29
12
|
|
30
|
-
task
|
13
|
+
# stands in for the Bundler-provided `build` task, which builds the
|
14
|
+
# gem for this project. Our release process builds the gems in a
|
15
|
+
# particular way, in a GitHub action. This task is just to help remind
|
16
|
+
# developers of that fact.
|
31
17
|
task :build do
|
32
|
-
|
18
|
+
abort <<~WARNING
|
19
|
+
`rake build` does nothing in this project. The gem must be built via
|
20
|
+
the `Mongoid Release` action on GitHub, which is triggered manually when
|
21
|
+
a new release is ready.
|
22
|
+
WARNING
|
33
23
|
end
|
34
24
|
|
35
|
-
|
36
|
-
|
25
|
+
# `rake version` is used by the deployment system so get the release version
|
26
|
+
# of the product beng deployed. It must do nothing more than just print the
|
27
|
+
# product version number.
|
28
|
+
#
|
29
|
+
# See the mongodb-labs/driver-github-tools/ruby/publish Github action.
|
30
|
+
desc "Print the current value of Mongoid::VERSION"
|
31
|
+
task :version do
|
32
|
+
require 'mongoid/version'
|
33
|
+
|
34
|
+
puts Mongoid::VERSION
|
37
35
|
end
|
38
36
|
|
37
|
+
# overrides the default Bundler-provided `release` task, which also
|
38
|
+
# builds the gem. Our release process assumes the gem has already
|
39
|
+
# been built (and signed via GPG), so we just need `rake release` to
|
40
|
+
# push the gem to rubygems.
|
39
41
|
task :release do
|
40
|
-
|
42
|
+
require 'mongoid/version'
|
43
|
+
|
44
|
+
if ENV['GITHUB_ACTION'].nil?
|
45
|
+
abort <<~WARNING
|
46
|
+
`rake release` must be invoked from the `Mongoid Release` GitHub action,
|
47
|
+
and must not be invoked locally. This ensures the gem is properly signed
|
48
|
+
and distributed by the appropriate user.
|
49
|
+
|
50
|
+
Note that it is the `rubygems/release-gem@v1` step in the `Mongoid Release`
|
51
|
+
action that invokes this task. Do not rename or remove this task, or the
|
52
|
+
release-gem step will fail. Reimplement this task with caution.
|
53
|
+
|
54
|
+
mongoid-#{Mongoid::VERSION}.gem was NOT pushed to RubyGems.
|
55
|
+
WARNING
|
56
|
+
end
|
57
|
+
|
58
|
+
system 'gem', 'push', "mongoid-#{Mongoid::VERSION}.gem"
|
41
59
|
end
|
42
60
|
|
43
61
|
RSpec::Core::RakeTask.new("spec") do |spec|
|
@@ -64,6 +82,8 @@ RUN_PRIORITY = %i(
|
|
64
82
|
)
|
65
83
|
|
66
84
|
def spec_organizer
|
85
|
+
require 'mrss/spec_organizer'
|
86
|
+
|
67
87
|
Mrss::SpecOrganizer.new(
|
68
88
|
root: ROOT,
|
69
89
|
classifiers: CLASSIFIERS,
|
@@ -99,32 +119,36 @@ task :docs => 'docs:yard'
|
|
99
119
|
namespace :docs do
|
100
120
|
desc "Generate yard documention"
|
101
121
|
task :yard do
|
122
|
+
require "mongoid/version"
|
123
|
+
|
102
124
|
out = File.join('yard-docs', Mongoid::VERSION)
|
103
125
|
FileUtils.rm_rf(out)
|
104
126
|
system "yardoc -o #{out} --title mongoid-#{Mongoid::VERSION}"
|
105
127
|
end
|
106
128
|
end
|
107
129
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
130
|
+
desc 'Build and validate the evergreen config'
|
131
|
+
task eg: %w[ eg:build eg:validate ]
|
132
|
+
|
133
|
+
# 'eg' == 'evergreen', but evergreen is too many letters for convenience
|
134
|
+
namespace :eg do
|
135
|
+
desc 'Builds the .evergreen/config.yml file from the templates'
|
136
|
+
task :build do
|
137
|
+
ruby '.evergreen/update-evergreen-configs'
|
113
138
|
end
|
114
|
-
end
|
115
139
|
|
116
|
-
desc '
|
117
|
-
task :
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
140
|
+
desc 'Validates the .evergreen/config.yml file'
|
141
|
+
task :validate do
|
142
|
+
system 'evergreen validate --project mongoid .evergreen/config.yml'
|
143
|
+
end
|
144
|
+
|
145
|
+
desc 'Updates the evergreen executable to the latest available version'
|
146
|
+
task :update do
|
147
|
+
system 'evergreen get-update --install'
|
148
|
+
end
|
149
|
+
|
150
|
+
desc 'Runs the current branch as an evergreen patch'
|
151
|
+
task :patch do
|
152
|
+
system 'evergreen patch --uncommitted --project mongoid --browse --auto-description --yes'
|
129
153
|
end
|
130
154
|
end
|
@@ -115,7 +115,11 @@ module Mongoid
|
|
115
115
|
# during binding or when cascading callbacks. Whenever we retrieve
|
116
116
|
# associations within the codebase, we use without_autobuild.
|
117
117
|
if !without_autobuild? && association.embedded? && attribute_missing?(field_name)
|
118
|
-
|
118
|
+
# We always allow accessing the parent document of an embedded one.
|
119
|
+
try_get_parent = association.is_a?(
|
120
|
+
Mongoid::Association::Embedded::EmbeddedIn
|
121
|
+
) && field_name == association.key
|
122
|
+
raise ActiveModel::MissingAttributeError, "Missing attribute: '#{field_name}'" unless try_get_parent
|
119
123
|
end
|
120
124
|
|
121
125
|
if !reload && (value = ivar(name)) != false
|
@@ -31,6 +31,9 @@ module Mongoid
|
|
31
31
|
docs_map = {}
|
32
32
|
queue = [ klass.to_s ]
|
33
33
|
|
34
|
+
# account for single-collection inheritance
|
35
|
+
queue.push(klass.root_class.to_s) if klass != klass.root_class
|
36
|
+
|
34
37
|
while klass = queue.shift
|
35
38
|
if as = assoc_map.delete(klass)
|
36
39
|
as.each do |assoc|
|
data/lib/mongoid/atomic.rb
CHANGED
@@ -178,13 +178,15 @@ module Mongoid
|
|
178
178
|
#
|
179
179
|
# @return [ Object ] The associated path.
|
180
180
|
def atomic_paths
|
181
|
-
@atomic_paths
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
181
|
+
return @atomic_paths if @atomic_paths
|
182
|
+
|
183
|
+
paths = if _association
|
184
|
+
_association.path(self)
|
185
|
+
else
|
186
|
+
Atomic::Paths::Root.new(self)
|
187
|
+
end
|
188
|
+
|
189
|
+
paths.tap { @atomic_paths = paths unless new_record? }
|
188
190
|
end
|
189
191
|
|
190
192
|
# Get all the attributes that need to be pulled.
|
data/lib/mongoid/config.rb
CHANGED
@@ -142,6 +142,16 @@ module Mongoid
|
|
142
142
|
end
|
143
143
|
end
|
144
144
|
|
145
|
+
# When this flag is true, callbacks for every embedded document will be
|
146
|
+
# called only once, even if the embedded document is embedded in multiple
|
147
|
+
# documents in the root document's dependencies graph.
|
148
|
+
# This will be the default in 9.0. Setting this flag to false restores the
|
149
|
+
# pre-9.0 behavior, where callbacks are called for every occurrence of an
|
150
|
+
# embedded document. The pre-9.0 behavior leads to a problem that for multi
|
151
|
+
# level nested documents callbacks are called multiple times.
|
152
|
+
# See https://jira.mongodb.org/browse/MONGOID-5542
|
153
|
+
option :prevent_multiple_calls_of_embedded_callbacks, default: false
|
154
|
+
|
145
155
|
# When this flag is true, callbacks for embedded documents will not be
|
146
156
|
# called. This is the default in 8.x, but will be changed to false in 9.0.
|
147
157
|
#
|
@@ -43,7 +43,21 @@ module Mongoid
|
|
43
43
|
#
|
44
44
|
# @return [ Object ] The converted number.
|
45
45
|
def __numeric__(object)
|
46
|
-
|
46
|
+
str = object.to_s
|
47
|
+
raise ArgumentError if str.empty?
|
48
|
+
|
49
|
+
# These requirements seem a bit odd, but they're explicitly specified in the tests,
|
50
|
+
# so we're obligated to keep them, for now. (This code was rewritten from a one-line
|
51
|
+
# regex, due to security concerns with a polynomial regex being used on uncontrolled
|
52
|
+
# data).
|
53
|
+
|
54
|
+
str = str.chop if str.end_with?('.')
|
55
|
+
return 0 if str.empty?
|
56
|
+
|
57
|
+
result = Integer(str) rescue Float(object)
|
58
|
+
|
59
|
+
integer = result.to_i
|
60
|
+
integer == result ? integer : result
|
47
61
|
end
|
48
62
|
|
49
63
|
# Evolve the object to an integer.
|
data/lib/mongoid/document.rb
CHANGED
@@ -133,7 +133,14 @@ module Mongoid
|
|
133
133
|
#
|
134
134
|
# @return [ Hash ] A hash of all attributes in the hierarchy.
|
135
135
|
def as_document
|
136
|
-
|
136
|
+
attrs = as_attributes
|
137
|
+
|
138
|
+
# legacy attributes have a tendency to leak internal state via
|
139
|
+
# `as_document`; we have to deep_dup the attributes here to prevent
|
140
|
+
# that.
|
141
|
+
attrs = attrs.deep_dup if Mongoid.legacy_attributes
|
142
|
+
|
143
|
+
BSON::Document.new(attrs)
|
137
144
|
end
|
138
145
|
|
139
146
|
# Calls #as_json on the document with additional, Mongoid-specific options.
|
data/lib/mongoid/fields.rb
CHANGED
@@ -47,6 +47,11 @@ module Mongoid
|
|
47
47
|
# @api private
|
48
48
|
INVALID_BSON_CLASSES = [ BSON::Decimal128, BSON::Int32, BSON::Int64 ].freeze
|
49
49
|
|
50
|
+
# The suffix for generated translated fields.
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
TRANSLATIONS_SFX = '_translations'
|
54
|
+
|
50
55
|
module ClassMethods
|
51
56
|
# Returns the list of id fields for this model class, as both strings
|
52
57
|
# and symbols.
|
@@ -99,8 +104,8 @@ module Mongoid
|
|
99
104
|
ar.each_with_index do |fn, i|
|
100
105
|
key = fn
|
101
106
|
unless klass.fields.key?(fn) || klass.relations.key?(fn)
|
102
|
-
if
|
103
|
-
key =
|
107
|
+
if fn.end_with?(TRANSLATIONS_SFX)
|
108
|
+
key = fn.delete_suffix(TRANSLATIONS_SFX)
|
104
109
|
else
|
105
110
|
key = fn
|
106
111
|
end
|
@@ -708,11 +713,11 @@ module Mongoid
|
|
708
713
|
# @param [ String ] meth The name of the method.
|
709
714
|
def create_translations_getter(name, meth)
|
710
715
|
generated_methods.module_eval do
|
711
|
-
re_define_method("#{meth}
|
716
|
+
re_define_method("#{meth}#{TRANSLATIONS_SFX}") do
|
712
717
|
attributes[name] ||= {}
|
713
718
|
attributes[name].with_indifferent_access
|
714
719
|
end
|
715
|
-
alias_method :"#{meth}_t", :"#{meth}
|
720
|
+
alias_method :"#{meth}_t", :"#{meth}#{TRANSLATIONS_SFX}"
|
716
721
|
end
|
717
722
|
end
|
718
723
|
|
@@ -726,14 +731,14 @@ module Mongoid
|
|
726
731
|
# @param [ Field ] field The field.
|
727
732
|
def create_translations_setter(name, meth, field)
|
728
733
|
generated_methods.module_eval do
|
729
|
-
re_define_method("#{meth}
|
734
|
+
re_define_method("#{meth}#{TRANSLATIONS_SFX}=") do |value|
|
730
735
|
attribute_will_change!(name)
|
731
736
|
value&.transform_values! do |_value|
|
732
737
|
field.type.mongoize(_value)
|
733
738
|
end
|
734
739
|
attributes[name] = value
|
735
740
|
end
|
736
|
-
alias_method :"#{meth}_t=", :"#{meth}
|
741
|
+
alias_method :"#{meth}_t=", :"#{meth}#{TRANSLATIONS_SFX}="
|
737
742
|
end
|
738
743
|
end
|
739
744
|
|
@@ -141,9 +141,13 @@ module Mongoid
|
|
141
141
|
# @api private
|
142
142
|
def _mongoid_run_child_callbacks(kind, children: nil, &block)
|
143
143
|
if Mongoid::Config.around_callbacks_for_embeds
|
144
|
-
_mongoid_run_child_callbacks_with_around(kind,
|
144
|
+
_mongoid_run_child_callbacks_with_around(kind,
|
145
|
+
children: children,
|
146
|
+
&block)
|
145
147
|
else
|
146
|
-
_mongoid_run_child_callbacks_without_around(kind,
|
148
|
+
_mongoid_run_child_callbacks_without_around(kind,
|
149
|
+
children: children,
|
150
|
+
&block)
|
147
151
|
end
|
148
152
|
end
|
149
153
|
|
@@ -163,13 +167,14 @@ module Mongoid
|
|
163
167
|
# @api private
|
164
168
|
def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
|
165
169
|
child, *tail = (children || cascadable_children(kind))
|
170
|
+
with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
|
166
171
|
if child.nil?
|
167
172
|
block&.call
|
168
173
|
elsif tail.empty?
|
169
|
-
child.run_callbacks(child_callback_type(kind, child), &block)
|
174
|
+
child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
|
170
175
|
else
|
171
|
-
child.run_callbacks(child_callback_type(kind, child)) do
|
172
|
-
|
176
|
+
child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
|
177
|
+
_mongoid_run_child_callbacks(kind, children: tail, &block)
|
173
178
|
end
|
174
179
|
end
|
175
180
|
end
|
@@ -218,9 +223,6 @@ module Mongoid
|
|
218
223
|
return false if env.halted
|
219
224
|
env.value = !env.halted
|
220
225
|
callback_list << [next_sequence, env]
|
221
|
-
if (grandchildren = child.send(:cascadable_children, kind))
|
222
|
-
_mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list)
|
223
|
-
end
|
224
226
|
end
|
225
227
|
callback_list
|
226
228
|
end
|
@@ -22,13 +22,20 @@ module Mongoid
|
|
22
22
|
# @example Set the created at time.
|
23
23
|
# person.set_created_at
|
24
24
|
def set_created_at
|
25
|
-
if
|
25
|
+
if able_to_set_created_at?
|
26
26
|
time = Time.configured.now
|
27
27
|
self.updated_at = time if is_a?(Updated) && !updated_at_changed?
|
28
28
|
self.created_at = time
|
29
29
|
end
|
30
30
|
clear_timeless_option
|
31
31
|
end
|
32
|
+
|
33
|
+
# Is the created timestamp able to be set?
|
34
|
+
#
|
35
|
+
# @return [ true, false ] If the timestamp can be set.
|
36
|
+
def able_to_set_created_at?
|
37
|
+
!frozen? && !timeless? && !created_at
|
38
|
+
end
|
32
39
|
end
|
33
40
|
end
|
34
41
|
end
|
data/lib/mongoid/traversable.rb
CHANGED
@@ -300,6 +300,18 @@ module Mongoid
|
|
300
300
|
!!(Mongoid::Document > superclass)
|
301
301
|
end
|
302
302
|
|
303
|
+
# Returns the root class of the STI tree that the current
|
304
|
+
# class participates in. If the class is not an STI subclass, this
|
305
|
+
# returns the class itself.
|
306
|
+
#
|
307
|
+
# @return [ Mongoid::Document ] the root of the STI tree
|
308
|
+
def root_class
|
309
|
+
root = self
|
310
|
+
root = root.superclass while root.hereditary?
|
311
|
+
|
312
|
+
root
|
313
|
+
end
|
314
|
+
|
303
315
|
# When inheriting, we want to copy the fields from the parent class and
|
304
316
|
# set the on the child to start, mimicking the behavior of the old
|
305
317
|
# class_inheritable_accessor that was deprecated in Rails edge.
|
@@ -15,32 +15,113 @@ module Mongoid
|
|
15
15
|
#
|
16
16
|
# validates_associated :name, :addresses
|
17
17
|
# end
|
18
|
-
class AssociatedValidator < ActiveModel::
|
18
|
+
class AssociatedValidator < ActiveModel::Validator
|
19
|
+
# Required by `validates_with` so that the validator
|
20
|
+
# gets added to the correct attributes.
|
21
|
+
def attributes
|
22
|
+
options[:attributes]
|
23
|
+
end
|
19
24
|
|
20
|
-
#
|
21
|
-
# valid.
|
22
|
-
# the
|
25
|
+
# Checks that the named associations of the given record
|
26
|
+
# (`attributes`) are valid. This does NOT load the associations
|
27
|
+
# from the database, and will only validate records that are dirty
|
28
|
+
# or unpersisted.
|
23
29
|
#
|
24
|
-
#
|
25
|
-
#
|
30
|
+
# If anything is not valid, appropriate errors will be added to
|
31
|
+
# the `document` parameter.
|
32
|
+
#
|
33
|
+
# @param [ Mongoid::Document ] document the document with the
|
34
|
+
# associations to validate.
|
35
|
+
def validate(document)
|
36
|
+
options[:attributes].each do |attr_name|
|
37
|
+
validate_association(document, attr_name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Validates that the given association provided is either nil,
|
44
|
+
# persisted and unchanged, or invalid. Otherwise, the appropriate errors
|
45
|
+
# will be added to the parent document.
|
26
46
|
#
|
27
47
|
# @param [ Document ] document The document to validate.
|
28
48
|
# @param [ Symbol ] attribute The association to validate.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
49
|
+
def validate_association(document, attribute)
|
50
|
+
# grab the proxy from the instance variable directly; we don't want
|
51
|
+
# any loading logic to run; we just want to see if it's already
|
52
|
+
# been loaded.
|
53
|
+
proxy = document.ivar(attribute)
|
54
|
+
return unless proxy
|
55
|
+
|
56
|
+
# if the variable exists, now we see if it is a proxy, or an actual
|
57
|
+
# document. It might be a literal document instead of a proxy if this
|
58
|
+
# document was created with a Document instance as a provided attribute,
|
59
|
+
# e.g. "Post.new(message: Message.new)".
|
60
|
+
target = proxy.respond_to?(:_target) ? proxy._target : proxy
|
61
|
+
|
62
|
+
# Now, fetch the list of documents from the target. Target may be a
|
63
|
+
# single value, or a list of values, and in the case of HasMany,
|
64
|
+
# might be a rather complex collection. We need to do this without
|
65
|
+
# triggering a load, so it's a bit of a delicate dance.
|
66
|
+
list = get_target_documents(target)
|
67
|
+
|
68
|
+
valid = document.validating do
|
69
|
+
# Now, treating the target as an array, look at each element
|
70
|
+
# and see if it is valid, but only if it has already been
|
71
|
+
# persisted, or changed, and hasn't been flagged for destroy.
|
72
|
+
#
|
73
|
+
# use map.all? instead of just all?, because all? will do short-circuit
|
74
|
+
# evaluation and terminate on the first failed validation.
|
75
|
+
list.map do |value|
|
76
|
+
if value && !value.flagged_for_destroy?
|
77
|
+
value.validated? ? true : value.valid?
|
36
78
|
else
|
37
|
-
|
79
|
+
true
|
38
80
|
end
|
39
81
|
end.all?
|
40
|
-
ensure
|
41
|
-
document.exit_validate
|
42
82
|
end
|
43
|
-
|
83
|
+
|
84
|
+
document.errors.add(attribute, :invalid) unless valid
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Examine the given target object and return an array of
|
90
|
+
# documents (possibly empty) that the target represents.
|
91
|
+
#
|
92
|
+
# @param [ Array | Mongoid::Document | Mongoid::Association::Proxy | HasMany::Enumerable ] target
|
93
|
+
# the target object to examine.
|
94
|
+
#
|
95
|
+
# @return [ Array<Mongoid::Document> ] the list of documents
|
96
|
+
def get_target_documents(target)
|
97
|
+
if target.respond_to?(:_loaded?)
|
98
|
+
get_target_documents_for_has_many(target)
|
99
|
+
else
|
100
|
+
get_target_documents_for_other(target)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns the list of all currently in-memory values held by
|
105
|
+
# the target. The target will not be loaded.
|
106
|
+
#
|
107
|
+
# @param [ HasMany::Enumerable ] target the target that will
|
108
|
+
# be examined for in-memory documents.
|
109
|
+
#
|
110
|
+
# @return [ Array<Mongoid::Document> ] the in-memory documents
|
111
|
+
# held by the target.
|
112
|
+
def get_target_documents_for_has_many(target)
|
113
|
+
[ *target._loaded.values, *target._added.values ]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the target as an array. If the target represents a single
|
117
|
+
# value, it is wrapped in an array.
|
118
|
+
#
|
119
|
+
# @param [ Array | Mongoid::Document | Mongoid::Association::Proxy ] target
|
120
|
+
# the target to return.
|
121
|
+
#
|
122
|
+
# @return [ Array<Mongoid::Document> ] the target, as an array.
|
123
|
+
def get_target_documents_for_other(target)
|
124
|
+
Array.wrap(target)
|
44
125
|
end
|
45
126
|
end
|
46
127
|
end
|
data/lib/mongoid/validatable.rb
CHANGED
@@ -37,6 +37,14 @@ module Mongoid
|
|
37
37
|
Threaded.exit_validate(self)
|
38
38
|
end
|
39
39
|
|
40
|
+
# Perform a validation within the associated block.
|
41
|
+
def validating
|
42
|
+
begin_validate
|
43
|
+
yield
|
44
|
+
ensure
|
45
|
+
exit_validate
|
46
|
+
end
|
47
|
+
|
40
48
|
# Given the provided options, are we performing validations?
|
41
49
|
#
|
42
50
|
# @example Are we performing validations?
|
data/lib/mongoid/version.rb
CHANGED
@@ -2,6 +2,28 @@
|
|
2
2
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
|
+
module HabtmSpec
|
6
|
+
class Page
|
7
|
+
include Mongoid::Document
|
8
|
+
embeds_many :blocks, class_name: 'HabtmSpec::Block'
|
9
|
+
end
|
10
|
+
|
11
|
+
class Block
|
12
|
+
include Mongoid::Document
|
13
|
+
embedded_in :page, class_name: 'HabtmSpec::Page'
|
14
|
+
end
|
15
|
+
|
16
|
+
class ImageBlock < Block
|
17
|
+
has_and_belongs_to_many :attachments, inverse_of: nil, class_name: 'HabtmSpec::Attachment'
|
18
|
+
accepts_nested_attributes_for :attachments
|
19
|
+
end
|
20
|
+
|
21
|
+
class Attachment
|
22
|
+
include Mongoid::Document
|
23
|
+
field :file, type: String
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
5
27
|
describe 'has_and_belongs_to_many associations' do
|
6
28
|
|
7
29
|
context 'when an anonymous class defines a has_and_belongs_to_many association' do
|
@@ -18,4 +40,22 @@ describe 'has_and_belongs_to_many associations' do
|
|
18
40
|
expect(klass.new.movies.build).to be_a Movie
|
19
41
|
end
|
20
42
|
end
|
43
|
+
|
44
|
+
context 'when an embedded has habtm relation' do
|
45
|
+
let(:attachment) { HabtmSpec::Attachment.create!(file: 'foo.jpg') }
|
46
|
+
|
47
|
+
let(:page) { HabtmSpec::Page.create! }
|
48
|
+
|
49
|
+
let(:image_block) do
|
50
|
+
image_block = page.blocks.build({
|
51
|
+
_type: 'HabtmSpec::ImageBlock',
|
52
|
+
attachment_ids: [ attachment.id.to_s ],
|
53
|
+
attachments_attributes: { '1234' => { file: 'bar.jpg', id: attachment.id.to_s } }
|
54
|
+
})
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'does not raise on save' do
|
58
|
+
expect { image_block.save! }.not_to raise_error
|
59
|
+
end
|
60
|
+
end
|
21
61
|
end
|
@@ -153,3 +153,40 @@ class Building
|
|
153
153
|
|
154
154
|
has_and_belongs_to_many :architects, dependent: :nullify
|
155
155
|
end
|
156
|
+
|
157
|
+
class Root
|
158
|
+
include Mongoid::Document
|
159
|
+
embeds_many :embedded_once, cascade_callbacks: true
|
160
|
+
after_save :trace
|
161
|
+
|
162
|
+
attr_accessor :logger
|
163
|
+
|
164
|
+
def trace
|
165
|
+
logger << :root
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class EmbeddedOnce
|
170
|
+
include Mongoid::Document
|
171
|
+
embeds_many :embedded_twice, cascade_callbacks: true
|
172
|
+
embedded_in :root
|
173
|
+
after_save :trace
|
174
|
+
|
175
|
+
attr_accessor :logger
|
176
|
+
|
177
|
+
def trace
|
178
|
+
logger << :embedded_once
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class EmbeddedTwice
|
183
|
+
include Mongoid::Document
|
184
|
+
embedded_in :embedded_once
|
185
|
+
after_save :trace
|
186
|
+
|
187
|
+
attr_accessor :logger
|
188
|
+
|
189
|
+
def trace
|
190
|
+
logger << :embedded_twice
|
191
|
+
end
|
192
|
+
end
|