mongoid_orderable 6.0.1 → 6.0.2
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/CHANGELOG.md +7 -1
- data/lib/mongoid/orderable/{engine.rb → handlers/base.rb} +42 -81
- data/lib/mongoid/orderable/handlers/document.rb +24 -0
- data/lib/mongoid/orderable/handlers/document_embedded.rb +14 -0
- data/lib/mongoid/orderable/handlers/document_transactional.rb +35 -0
- data/lib/mongoid/orderable/handlers/transaction.rb +71 -0
- data/lib/mongoid/orderable/mixins/callbacks.rb +21 -7
- data/lib/mongoid/orderable/version.rb +1 -1
- data/lib/mongoid_orderable.rb +5 -1
- data/spec/integration/concurrency_spec.rb +232 -0
- data/spec/integration/customized_spec.rb +31 -0
- data/spec/integration/embedded_spec.rb +41 -0
- data/spec/integration/foreign_key_spec.rb +33 -0
- data/spec/integration/inherited_spec.rb +54 -0
- data/spec/integration/multiple_fields_spec.rb +554 -0
- data/spec/integration/multiple_scoped_spec.rb +63 -0
- data/spec/integration/no_indexed_spec.rb +23 -0
- data/spec/integration/scoped_spec.rb +151 -0
- data/spec/integration/simple_spec.rb +184 -0
- data/spec/integration/string_scoped_spec.rb +28 -0
- data/spec/integration/zero_based_spec.rb +161 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/models.rb +122 -0
- metadata +33 -5
- data/spec/mongoid/orderable_spec.rb +0 -1486
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3a80c8eb058adeffe1ead4c0fa20ce10660ea49a2df1405d46048419f2af965
|
4
|
+
data.tar.gz: d68f6f64f52f132295e588dfcd59ad27d22a68237e5b29ab7682a37e3c940ca5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c2d9da294dda836b17047aaf3ebc6329b3c382fb793c83fe57dd63a07512d2e1da65af3559b12e75ce3924b1edfaaefad8317f4acaa488cb94ad2392d84b474
|
7
|
+
data.tar.gz: 58e8c15554183674cef20639c2cb888cb6e793471ecf7f55ea76360f2ca7de351d218f417e34db220d2daa32c6ab5dc11da23bce7acec0f77a1f99718e713705
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,13 @@
|
|
1
|
-
### 6.0.
|
1
|
+
### 6.0.3 (Next)
|
2
2
|
|
3
3
|
* Your contribution here.
|
4
4
|
|
5
|
+
### 6.0.2 (2021/01/26)
|
6
|
+
|
7
|
+
* [#70](https://github.com/mongoid/mongoid_orderable/pull/70): Fix: Transactions should not use around callbacks - [@johnnyshields](https://github.com/johnnyshields).
|
8
|
+
* [#70](https://github.com/mongoid/mongoid_orderable/pull/70): Refactor: Partially reduce code complexity of handler classes - [@johnnyshields](https://github.com/johnnyshields).
|
9
|
+
* [#70](https://github.com/mongoid/mongoid_orderable/pull/70): Tests: Run all tests both with and without transactions - [@johnnyshields](https://github.com/johnnyshields).
|
10
|
+
|
5
11
|
### 6.0.1 (2021/01/26)
|
6
12
|
|
7
13
|
* [#69](https://github.com/mongoid/mongoid_orderable/pull/69): Fix: Transactions should force read from primary - [@johnnyshields](https://github.com/johnnyshields).
|
@@ -2,46 +2,53 @@
|
|
2
2
|
|
3
3
|
module Mongoid
|
4
4
|
module Orderable
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
attr_accessor :doc
|
5
|
+
module Handlers
|
6
|
+
class Base
|
7
|
+
attr_reader :doc
|
9
8
|
|
10
9
|
def initialize(doc)
|
11
10
|
@doc = doc
|
12
11
|
end
|
13
12
|
|
14
|
-
|
15
|
-
# we must yield the save action inside the transaction.
|
16
|
-
def update_positions(&_block)
|
17
|
-
yield and return unless orderable_keys.any? {|field| changed?(field) }
|
13
|
+
protected
|
18
14
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
15
|
+
delegate :orderable_keys,
|
16
|
+
:orderable_field,
|
17
|
+
:orderable_position,
|
18
|
+
:orderable_scope,
|
19
|
+
:orderable_scope_changed?,
|
20
|
+
:orderable_top,
|
21
|
+
:orderable_bottom,
|
22
|
+
:_id,
|
23
|
+
:new_record?,
|
24
|
+
:persisted?,
|
25
|
+
:embedded?,
|
26
|
+
:collection_name,
|
27
|
+
to: :doc
|
24
28
|
|
25
|
-
|
29
|
+
def use_transactions
|
30
|
+
false
|
26
31
|
end
|
27
32
|
|
28
|
-
def
|
29
|
-
orderable_keys.
|
30
|
-
|
31
|
-
|
33
|
+
def any_field_changed?
|
34
|
+
orderable_keys.any? {|field| changed?(field) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def apply_all_positions
|
38
|
+
orderable_keys.map {|field| apply_one_position(field, move_all[field]) }
|
32
39
|
end
|
33
40
|
|
34
41
|
def apply_one_position(field, target_position)
|
35
42
|
return unless changed?(field)
|
36
43
|
|
37
|
-
set_lock(field) if use_transactions
|
44
|
+
set_lock(field) if use_transactions
|
38
45
|
|
39
46
|
f = orderable_field(field)
|
40
47
|
scope = orderable_scope(field)
|
41
48
|
scope_changed = orderable_scope_changed?(field)
|
42
49
|
|
43
50
|
# Set scope-level lock if scope changed
|
44
|
-
if use_transactions && persisted? &&
|
51
|
+
if use_transactions && persisted? && scope_changed
|
45
52
|
set_lock(field, true)
|
46
53
|
scope_changed = orderable_scope_changed?(field)
|
47
54
|
end
|
@@ -58,7 +65,7 @@ module Orderable
|
|
58
65
|
# If scope changed, remove the position from the old scope
|
59
66
|
if persisted? && !embedded? && scope_changed
|
60
67
|
existing_doc = doc.class.unscoped.find(_id)
|
61
|
-
self.class.new(existing_doc).remove_one_position
|
68
|
+
self.class.new(existing_doc).send(:remove_one_position, field)
|
62
69
|
end
|
63
70
|
|
64
71
|
# Return if there is no instruction to change the position
|
@@ -76,33 +83,23 @@ module Orderable
|
|
76
83
|
end
|
77
84
|
|
78
85
|
# If persisted, update the field in the database atomically
|
79
|
-
doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted?
|
86
|
+
doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted?
|
80
87
|
doc.send("orderable_#{field}_position=", target)
|
81
88
|
end
|
82
89
|
|
90
|
+
def remove_all_positions
|
91
|
+
orderable_keys.each do |field|
|
92
|
+
remove_one_position(field)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
83
96
|
def remove_one_position(field)
|
84
97
|
f = orderable_field(field)
|
85
98
|
current = orderable_position(field)
|
86
|
-
set_lock(field) if use_transactions
|
99
|
+
set_lock(field) if use_transactions
|
87
100
|
orderable_scope(field).gt(f => current).inc(f => -1)
|
88
101
|
end
|
89
102
|
|
90
|
-
protected
|
91
|
-
|
92
|
-
delegate :orderable_keys,
|
93
|
-
:orderable_field,
|
94
|
-
:orderable_position,
|
95
|
-
:orderable_scope,
|
96
|
-
:orderable_scope_changed?,
|
97
|
-
:orderable_top,
|
98
|
-
:orderable_bottom,
|
99
|
-
:_id,
|
100
|
-
:new_record?,
|
101
|
-
:persisted?,
|
102
|
-
:embedded?,
|
103
|
-
:collection_name,
|
104
|
-
to: :doc
|
105
|
-
|
106
103
|
def move_all
|
107
104
|
doc.send(:move_all)
|
108
105
|
end
|
@@ -151,56 +148,20 @@ module Orderable
|
|
151
148
|
end
|
152
149
|
end
|
153
150
|
|
154
|
-
def set_lock(field,
|
155
|
-
return unless use_transactions
|
151
|
+
def set_lock(field, generic = false)
|
152
|
+
return unless use_transactions
|
156
153
|
model_name = doc.class.orderable_configs[field][:lock_collection].to_s.singularize.classify
|
157
154
|
model = Mongoid::Orderable::Models.const_get(model_name)
|
158
|
-
attrs = lock_scope(field,
|
155
|
+
attrs = lock_scope(field, generic)
|
159
156
|
model.where(attrs).find_one_and_update(attrs, { upsert: true })
|
160
157
|
end
|
161
158
|
|
162
|
-
def lock_scope(field,
|
159
|
+
def lock_scope(field, generic = false)
|
163
160
|
sel = orderable_scope(field).selector
|
164
|
-
scope = ([collection_name] + (
|
161
|
+
scope = ([collection_name] + (generic ? [field] : sel.to_a.flatten)).map(&:to_s).join('|')
|
165
162
|
{ scope: scope }
|
166
163
|
end
|
167
|
-
|
168
|
-
def use_transactions
|
169
|
-
orderable_keys.any? {|k| doc.class.orderable_configs[k][:use_transactions] }
|
170
|
-
end
|
171
|
-
|
172
|
-
def transaction_max_retries
|
173
|
-
orderable_keys.map {|k| doc.class.orderable_configs[k][:transaction_max_retries] }.compact.max
|
174
|
-
end
|
175
|
-
|
176
|
-
def with_transaction(&_block)
|
177
|
-
Mongoid::QueryCache.uncached do
|
178
|
-
if use_transactions && !embedded? && !Thread.current[ORDERABLE_TRANSACTION_KEY]
|
179
|
-
Thread.current[ORDERABLE_TRANSACTION_KEY] = true
|
180
|
-
retries = transaction_max_retries
|
181
|
-
begin
|
182
|
-
doc.class.with_session(causal_consistency: true) do |session|
|
183
|
-
doc.class.with(read: { mode: :primary }) do
|
184
|
-
session.start_transaction(read: { mode: :primary },
|
185
|
-
read_concern: { level: 'majority' },
|
186
|
-
write_concern: { w: 'majority' })
|
187
|
-
yield
|
188
|
-
session.commit_transaction
|
189
|
-
end
|
190
|
-
end
|
191
|
-
rescue Mongo::Error::OperationFailure => e
|
192
|
-
sleep(0.001)
|
193
|
-
retries -= 1
|
194
|
-
retry if retries >= 0
|
195
|
-
raise Mongoid::Orderable::Errors::TransactionFailed.new(e)
|
196
|
-
ensure
|
197
|
-
Thread.current[ORDERABLE_TRANSACTION_KEY] = nil
|
198
|
-
end
|
199
|
-
else
|
200
|
-
yield
|
201
|
-
end
|
202
|
-
end
|
203
|
-
end
|
204
164
|
end
|
205
165
|
end
|
206
166
|
end
|
167
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
class Document < Base
|
7
|
+
def before_create
|
8
|
+
apply_all_positions
|
9
|
+
end
|
10
|
+
|
11
|
+
def after_create; end
|
12
|
+
|
13
|
+
def before_update
|
14
|
+
return unless any_field_changed?
|
15
|
+
apply_all_positions
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_destroy
|
19
|
+
remove_all_positions
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
class DocumentTransactional < Document
|
7
|
+
def before_create
|
8
|
+
clear_all_positions
|
9
|
+
end
|
10
|
+
|
11
|
+
def after_create
|
12
|
+
apply_all_positions
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def apply_all_positions
|
18
|
+
with_transaction { super }
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_all_positions
|
22
|
+
orderable_keys.each {|field| doc.send("orderable_#{field}_position=", nil) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_transactions
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_transaction(&block)
|
30
|
+
Mongoid::Orderable::Handlers::Transaction.new(doc).with_transaction(&block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
# Executes a block within the context of a MongoDB transaction.
|
7
|
+
class Transaction
|
8
|
+
THREAD_KEY = :__mongoid_orderable_in_txn
|
9
|
+
RETRY_SLEEP = 0.001
|
10
|
+
|
11
|
+
attr_reader :doc
|
12
|
+
|
13
|
+
def initialize(doc)
|
14
|
+
@doc = doc
|
15
|
+
end
|
16
|
+
|
17
|
+
def with_transaction(&block)
|
18
|
+
Mongoid::QueryCache.uncached do
|
19
|
+
if Thread.current[THREAD_KEY]
|
20
|
+
yield
|
21
|
+
else
|
22
|
+
Thread.current[THREAD_KEY] = true
|
23
|
+
retries = transaction_max_retries
|
24
|
+
begin
|
25
|
+
do_transaction(&block)
|
26
|
+
rescue Mongo::Error::OperationFailure => e
|
27
|
+
sleep(RETRY_SLEEP)
|
28
|
+
retries -= 1
|
29
|
+
retry if retries >= 0
|
30
|
+
raise Mongoid::Orderable::Errors::TransactionFailed.new(e)
|
31
|
+
ensure
|
32
|
+
Thread.current[THREAD_KEY] = nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def do_transaction(&_block)
|
41
|
+
doc.class.with_session(session_opts) do |session|
|
42
|
+
doc.class.with(persistence_opts) do
|
43
|
+
session.start_transaction(transaction_opts)
|
44
|
+
yield
|
45
|
+
session.commit_transaction
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def session_opts
|
51
|
+
{ read: { mode: :primary },
|
52
|
+
causal_consistency: true }
|
53
|
+
end
|
54
|
+
|
55
|
+
def persistence_opts
|
56
|
+
{ read: { mode: :primary } }
|
57
|
+
end
|
58
|
+
|
59
|
+
def transaction_opts
|
60
|
+
{ read: { mode: :primary },
|
61
|
+
read_concern: { level: 'majority' },
|
62
|
+
write_concern: { w: 'majority' } }
|
63
|
+
end
|
64
|
+
|
65
|
+
def transaction_max_retries
|
66
|
+
doc.orderable_keys.map {|k| doc.class.orderable_configs.dig(k, :transaction_max_retries) }.compact.max
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -9,18 +9,32 @@ module Mixins
|
|
9
9
|
ORDERABLE_TRANSACTION_KEY = :__mongoid_orderable_in_txn
|
10
10
|
|
11
11
|
included do
|
12
|
-
|
13
|
-
|
12
|
+
before_create :orderable_before_create
|
13
|
+
after_create :orderable_after_create, prepend: true
|
14
|
+
before_update :orderable_before_update
|
15
|
+
after_destroy :orderable_after_destroy, prepend: true
|
14
16
|
|
15
|
-
delegate :
|
16
|
-
:
|
17
|
-
|
17
|
+
delegate :before_create,
|
18
|
+
:after_create,
|
19
|
+
:before_update,
|
20
|
+
:after_destroy,
|
21
|
+
to: :orderable_handler,
|
18
22
|
prefix: :orderable
|
19
23
|
|
20
24
|
protected
|
21
25
|
|
22
|
-
def
|
23
|
-
@
|
26
|
+
def orderable_handler
|
27
|
+
@orderable_handler ||= self.class.orderable_handler_class.new(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.orderable_handler_class
|
31
|
+
if embedded?
|
32
|
+
Mongoid::Orderable::Handlers::DocumentEmbedded
|
33
|
+
elsif orderable_configs.values.any? {|c| c[:use_transactions] }
|
34
|
+
Mongoid::Orderable::Handlers::DocumentTransactional
|
35
|
+
else
|
36
|
+
Mongoid::Orderable::Handlers::Document
|
37
|
+
end
|
24
38
|
end
|
25
39
|
end
|
26
40
|
end
|
data/lib/mongoid_orderable.rb
CHANGED
@@ -25,5 +25,9 @@ require 'mongoid/orderable/generators/movable'
|
|
25
25
|
require 'mongoid/orderable/generators/position'
|
26
26
|
require 'mongoid/orderable/generators/scope'
|
27
27
|
require 'mongoid/orderable/generators/helpers'
|
28
|
-
require 'mongoid/orderable/
|
28
|
+
require 'mongoid/orderable/handlers/base'
|
29
|
+
require 'mongoid/orderable/handlers/document'
|
30
|
+
require 'mongoid/orderable/handlers/document_embedded'
|
31
|
+
require 'mongoid/orderable/handlers/document_transactional'
|
32
|
+
require 'mongoid/orderable/handlers/transaction'
|
29
33
|
require 'mongoid/orderable/installer'
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'concurrency' do
|
4
|
+
enable_transactions!
|
5
|
+
|
6
|
+
describe 'simple create' do
|
7
|
+
|
8
|
+
it 'should correctly insert at the top' do
|
9
|
+
20.times.map do
|
10
|
+
Thread.new do
|
11
|
+
SimpleOrderable.create!(move_to: :top)
|
12
|
+
end
|
13
|
+
end.each(&:join)
|
14
|
+
|
15
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq((1..20).to_a)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should correctly insert at the bottom' do
|
19
|
+
20.times.map do
|
20
|
+
Thread.new do
|
21
|
+
SimpleOrderable.create!
|
22
|
+
end
|
23
|
+
end.each(&:join)
|
24
|
+
|
25
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq((1..20).to_a)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should correctly insert at a random position' do
|
29
|
+
20.times.map do
|
30
|
+
Thread.new do
|
31
|
+
SimpleOrderable.create!(move_to: (1..10).to_a.sample)
|
32
|
+
end
|
33
|
+
end.each(&:join)
|
34
|
+
|
35
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq((1..20).to_a)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'simple update' do
|
40
|
+
before :each do
|
41
|
+
5.times { SimpleOrderable.create! }
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should correctly move items to top' do
|
45
|
+
20.times.map do
|
46
|
+
Thread.new do
|
47
|
+
record = SimpleOrderable.all.sample
|
48
|
+
record.update_attributes move_to: :top
|
49
|
+
end
|
50
|
+
end.each(&:join)
|
51
|
+
|
52
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq([1, 2, 3, 4, 5])
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should correctly move items to bottom' do
|
56
|
+
20.times.map do
|
57
|
+
Thread.new do
|
58
|
+
record = SimpleOrderable.all.sample
|
59
|
+
record.update_attributes move_to: :bottom
|
60
|
+
end
|
61
|
+
end.each(&:join)
|
62
|
+
|
63
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq([1, 2, 3, 4, 5])
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should correctly move items higher' do
|
67
|
+
20.times.map do
|
68
|
+
Thread.new do
|
69
|
+
record = SimpleOrderable.all.sample
|
70
|
+
record.update_attributes move_to: :higher
|
71
|
+
end
|
72
|
+
end.each(&:join)
|
73
|
+
|
74
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq([1, 2, 3, 4, 5])
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should correctly move items lower' do
|
78
|
+
20.times.map do
|
79
|
+
Thread.new do
|
80
|
+
record = SimpleOrderable.all.sample
|
81
|
+
record.update_attributes move_to: :lower
|
82
|
+
end
|
83
|
+
end.each(&:join)
|
84
|
+
|
85
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq([1, 2, 3, 4, 5])
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should correctly insert at the top' do
|
89
|
+
20.times.map do
|
90
|
+
Thread.new do
|
91
|
+
SimpleOrderable.create!(move_to: :top)
|
92
|
+
end
|
93
|
+
end.each(&:join)
|
94
|
+
|
95
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq((1..25).to_a)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should correctly insert at the bottom' do
|
99
|
+
20.times.map do
|
100
|
+
Thread.new do
|
101
|
+
SimpleOrderable.create!
|
102
|
+
end
|
103
|
+
end.each(&:join)
|
104
|
+
|
105
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq((1..25).to_a)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should correctly insert at a random position' do
|
109
|
+
20.times.map do
|
110
|
+
Thread.new do
|
111
|
+
SimpleOrderable.create!(move_to: (1..10).to_a.sample)
|
112
|
+
end
|
113
|
+
end.each(&:join)
|
114
|
+
|
115
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq((1..25).to_a)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should correctly move items to a random position' do
|
119
|
+
20.times.map do
|
120
|
+
Thread.new do
|
121
|
+
record = SimpleOrderable.all.sample
|
122
|
+
record.update_attributes move_to: (1..5).to_a.sample
|
123
|
+
end
|
124
|
+
end.each(&:join)
|
125
|
+
|
126
|
+
expect(SimpleOrderable.pluck(:position).sort).to eq([1, 2, 3, 4, 5])
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'scoped update' do
|
131
|
+
|
132
|
+
before :each do
|
133
|
+
2.times { ScopedOrderable.create! group_id: 1 }
|
134
|
+
3.times { ScopedOrderable.create! group_id: 2 }
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should correctly move items to top' do
|
138
|
+
20.times.map do
|
139
|
+
Thread.new do
|
140
|
+
record = ScopedOrderable.all.sample
|
141
|
+
record.update_attributes move_to: :top
|
142
|
+
end
|
143
|
+
end.each(&:join)
|
144
|
+
|
145
|
+
expect(ScopedOrderable.pluck(:position).sort).to eq([1, 1, 2, 2, 3])
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'should correctly move items to bottom' do
|
149
|
+
20.times.map do
|
150
|
+
Thread.new do
|
151
|
+
record = ScopedOrderable.all.sample
|
152
|
+
record.update_attributes move_to: :bottom
|
153
|
+
end
|
154
|
+
end.each(&:join)
|
155
|
+
|
156
|
+
expect(ScopedOrderable.pluck(:position).sort).to eq([1, 1, 2, 2, 3])
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should correctly move items higher' do
|
160
|
+
20.times.map do
|
161
|
+
Thread.new do
|
162
|
+
record = ScopedOrderable.all.sample
|
163
|
+
record.update_attributes move_to: :higher
|
164
|
+
end
|
165
|
+
end.each(&:join)
|
166
|
+
|
167
|
+
expect(ScopedOrderable.pluck(:position).sort).to eq([1, 1, 2, 2, 3])
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should correctly move items lower' do
|
171
|
+
20.times.map do
|
172
|
+
Thread.new do
|
173
|
+
record = ScopedOrderable.all.sample
|
174
|
+
record.update_attributes move_to: :lower
|
175
|
+
end
|
176
|
+
end.each(&:join)
|
177
|
+
|
178
|
+
expect(ScopedOrderable.pluck(:position).sort).to eq([1, 1, 2, 2, 3])
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'should correctly move items to a random position' do
|
182
|
+
20.times.map do
|
183
|
+
Thread.new do
|
184
|
+
record = ScopedOrderable.all.sample
|
185
|
+
record.update_attributes move_to: (1..5).to_a.sample
|
186
|
+
end
|
187
|
+
end.each(&:join)
|
188
|
+
|
189
|
+
expect(ScopedOrderable.pluck(:position).sort).to eq([1, 1, 2, 2, 3])
|
190
|
+
end
|
191
|
+
|
192
|
+
# This spec fails randomly
|
193
|
+
it 'should correctly move items to a random scope', retry: 5 do
|
194
|
+
20.times.map do
|
195
|
+
Thread.new do
|
196
|
+
record = ScopedOrderable.all.sample
|
197
|
+
group_id = ([1, 2, 3] - [record.group_id]).sample
|
198
|
+
record.update_attributes group_id: group_id
|
199
|
+
end
|
200
|
+
end.each(&:join)
|
201
|
+
|
202
|
+
result = ScopedOrderable.all.to_a.each_with_object({}) do |obj, hash|
|
203
|
+
hash[obj.group_id] ||= []
|
204
|
+
hash[obj.group_id] << obj.position
|
205
|
+
end
|
206
|
+
|
207
|
+
result.values.each do |ary|
|
208
|
+
expect(ary.sort).to eq((1..(ary.size)).to_a)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should correctly move items to a random position and scope' do
|
213
|
+
20.times.map do
|
214
|
+
Thread.new do
|
215
|
+
record = ScopedOrderable.all.sample
|
216
|
+
group_id = ([1, 2, 3] - [record.group_id]).sample
|
217
|
+
position = (1..5).to_a.sample
|
218
|
+
record.update_attributes group_id: group_id, move_to: position
|
219
|
+
end
|
220
|
+
end.each(&:join)
|
221
|
+
|
222
|
+
result = ScopedOrderable.all.to_a.each_with_object({}) do |obj, hash|
|
223
|
+
hash[obj.group_id] ||= []
|
224
|
+
hash[obj.group_id] << obj.position
|
225
|
+
end
|
226
|
+
|
227
|
+
result.values.each do |ary|
|
228
|
+
expect(ary.sort).to eq((1..(ary.size)).to_a)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|