acts_as_list 0.7.4 → 1.1.0
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 +5 -13
- data/.github/FUNDING.yml +3 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +123 -0
- data/.gitignore +1 -0
- data/.travis.yml +50 -12
- data/Appraisals +39 -6
- data/CHANGELOG.md +565 -148
- data/Gemfile +19 -14
- data/README.md +206 -19
- data/Rakefile +4 -4
- data/acts_as_list.gemspec +16 -11
- data/gemfiles/rails_4_2.gemfile +18 -9
- data/gemfiles/rails_5_0.gemfile +31 -0
- data/gemfiles/rails_5_1.gemfile +31 -0
- data/gemfiles/rails_5_2.gemfile +31 -0
- data/gemfiles/rails_6_0.gemfile +31 -0
- data/gemfiles/rails_6_1.gemfile +31 -0
- data/gemfiles/rails_7_0.gemfile +31 -0
- data/init.rb +2 -0
- data/lib/acts_as_list/active_record/acts/active_record.rb +5 -0
- data/lib/acts_as_list/active_record/acts/add_new_at_method_definer.rb +11 -0
- data/lib/acts_as_list/active_record/acts/aux_method_definer.rb +11 -0
- data/lib/acts_as_list/active_record/acts/callback_definer.rb +19 -0
- data/lib/acts_as_list/active_record/acts/list.rb +299 -306
- data/lib/acts_as_list/active_record/acts/no_update.rb +125 -0
- data/lib/acts_as_list/active_record/acts/position_column_method_definer.rb +101 -0
- data/lib/acts_as_list/active_record/acts/scope_method_definer.rb +77 -0
- data/lib/acts_as_list/active_record/acts/sequential_updates_method_definer.rb +28 -0
- data/lib/acts_as_list/active_record/acts/top_of_list_method_definer.rb +15 -0
- data/lib/acts_as_list/version.rb +3 -1
- data/lib/acts_as_list.rb +11 -14
- data/test/database.yml +18 -0
- data/test/helper.rb +50 -2
- data/test/shared.rb +3 -0
- data/test/shared_array_scope_list.rb +21 -4
- data/test/shared_list.rb +86 -12
- data/test/shared_list_sub.rb +63 -2
- data/test/shared_no_addition.rb +50 -2
- data/test/shared_quoting.rb +23 -0
- data/test/shared_top_addition.rb +36 -13
- data/test/shared_zero_based.rb +13 -0
- data/test/test_default_scope_with_select.rb +33 -0
- data/test/test_joined_list.rb +61 -0
- data/test/test_list.rb +601 -84
- data/test/test_no_update_for_extra_classes.rb +131 -0
- data/test/test_no_update_for_scope_destruction.rb +69 -0
- data/test/test_no_update_for_subclasses.rb +56 -0
- data/test/test_scope_with_user_defined_foreign_key.rb +42 -0
- metadata +56 -22
- data/gemfiles/rails_3_2.gemfile +0 -24
- data/gemfiles/rails_4_1.gemfile +0 -24
@@ -1,27 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActiveRecord
|
2
4
|
module Acts #:nodoc:
|
3
5
|
module List #:nodoc:
|
4
|
-
def self.included(base)
|
5
|
-
base.extend(ClassMethods)
|
6
|
-
end
|
7
6
|
|
8
|
-
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
|
9
|
-
# The class that has this specified needs to have a +position+ column defined as an integer on
|
10
|
-
# the mapped database table.
|
11
|
-
#
|
12
|
-
# Todo list example:
|
13
|
-
#
|
14
|
-
# class TodoList < ActiveRecord::Base
|
15
|
-
# has_many :todo_items, order: "position"
|
16
|
-
# end
|
17
|
-
#
|
18
|
-
# class TodoItem < ActiveRecord::Base
|
19
|
-
# belongs_to :todo_list
|
20
|
-
# acts_as_list scope: :todo_list
|
21
|
-
# end
|
22
|
-
#
|
23
|
-
# todo_list.first.move_to_bottom
|
24
|
-
# todo_list.last.move_higher
|
25
7
|
module ClassMethods
|
26
8
|
# Configuration options are:
|
27
9
|
#
|
@@ -33,121 +15,82 @@ module ActiveRecord
|
|
33
15
|
# * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
|
34
16
|
# act more like an array in its indexing.
|
35
17
|
# * +add_new_at+ - specifies whether objects get added to the :top or :bottom of the list. (default: +bottom+)
|
36
|
-
# `nil` will result in new items not being added to the list on create
|
18
|
+
# `nil` will result in new items not being added to the list on create.
|
19
|
+
# * +sequential_updates+ - specifies whether insert_at should update objects positions during shuffling
|
20
|
+
# one by one to respect position column unique not null constraint.
|
21
|
+
# Defaults to true if position column has unique index, otherwise false.
|
22
|
+
# If constraint is <tt>deferrable initially deferred<tt>, overriding it with false will speed up insert_at.
|
23
|
+
# * +touch_on_update+ - configuration to disable the update of the model timestamps when the positions are updated.
|
37
24
|
def acts_as_list(options = {})
|
38
|
-
configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom}
|
25
|
+
configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom, touch_on_update: true }
|
39
26
|
configuration.update(options) if options.is_a?(Hash)
|
40
27
|
|
41
|
-
|
42
|
-
configuration[:scope] = :"#{configuration[:scope]}_id"
|
43
|
-
end
|
44
|
-
|
45
|
-
if configuration[:scope].is_a?(Symbol)
|
46
|
-
scope_methods = %(
|
47
|
-
def scope_condition
|
48
|
-
{ #{configuration[:scope]}: send(:#{configuration[:scope]}) }
|
49
|
-
end
|
50
|
-
|
51
|
-
def scope_changed?
|
52
|
-
changed.include?(scope_name.to_s)
|
53
|
-
end
|
54
|
-
)
|
55
|
-
elsif configuration[:scope].is_a?(Array)
|
56
|
-
scope_methods = %(
|
57
|
-
def scope_condition
|
58
|
-
#{configuration[:scope]}.inject({}) do |hash, column|
|
59
|
-
hash.merge!({ column.to_sym => read_attribute(column.to_sym) })
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def scope_changed?
|
64
|
-
(scope_condition.keys & changed.map(&:to_sym)).any?
|
65
|
-
end
|
66
|
-
)
|
67
|
-
else
|
68
|
-
scope_methods = %(
|
69
|
-
def scope_condition
|
70
|
-
"#{configuration[:scope]}"
|
71
|
-
end
|
72
|
-
|
73
|
-
def scope_changed?() false end
|
74
|
-
)
|
75
|
-
end
|
76
|
-
|
77
|
-
class_eval <<-EOV, __FILE__, __LINE__ + 1
|
78
|
-
include ::ActiveRecord::Acts::List::InstanceMethods
|
79
|
-
|
80
|
-
def acts_as_list_top
|
81
|
-
#{configuration[:top_of_list]}.to_i
|
82
|
-
end
|
83
|
-
|
84
|
-
def acts_as_list_class
|
85
|
-
::#{self.name}
|
86
|
-
end
|
87
|
-
|
88
|
-
def position_column
|
89
|
-
'#{configuration[:column]}'
|
90
|
-
end
|
28
|
+
caller_class = self
|
91
29
|
|
92
|
-
|
93
|
-
|
94
|
-
|
30
|
+
ActiveRecord::Acts::List::PositionColumnMethodDefiner.call(caller_class, configuration[:column], configuration[:touch_on_update])
|
31
|
+
ActiveRecord::Acts::List::ScopeMethodDefiner.call(caller_class, configuration[:scope])
|
32
|
+
ActiveRecord::Acts::List::TopOfListMethodDefiner.call(caller_class, configuration[:top_of_list])
|
33
|
+
ActiveRecord::Acts::List::AddNewAtMethodDefiner.call(caller_class, configuration[:add_new_at])
|
95
34
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
def #{configuration[:column]}=(position)
|
101
|
-
write_attribute(:#{configuration[:column]}, position)
|
102
|
-
@position_changed = true
|
103
|
-
end
|
104
|
-
|
105
|
-
#{scope_methods}
|
106
|
-
|
107
|
-
# only add to attr_accessible
|
108
|
-
# if the class has some mass_assignment_protection
|
109
|
-
|
110
|
-
if defined?(accessible_attributes) and !accessible_attributes.blank?
|
111
|
-
attr_accessible :#{configuration[:column]}
|
112
|
-
end
|
35
|
+
ActiveRecord::Acts::List::AuxMethodDefiner.call(caller_class)
|
36
|
+
ActiveRecord::Acts::List::CallbackDefiner.call(caller_class, configuration[:add_new_at])
|
37
|
+
ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner.call(caller_class, configuration[:column], configuration[:sequential_updates])
|
113
38
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
after_destroy :decrement_positions_on_lower_items
|
118
|
-
|
119
|
-
before_update :check_scope
|
120
|
-
after_update :update_positions
|
121
|
-
|
122
|
-
after_commit 'remove_instance_variable(:@scope_changed) if defined?(@scope_changed)'
|
123
|
-
|
124
|
-
scope :in_list, lambda { where("#{table_name}.#{configuration[:column]} IS NOT NULL") }
|
125
|
-
EOV
|
39
|
+
include ActiveRecord::Acts::List::InstanceMethods
|
40
|
+
include ActiveRecord::Acts::List::NoUpdate
|
41
|
+
end
|
126
42
|
|
127
|
-
|
128
|
-
|
129
|
-
|
43
|
+
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
|
44
|
+
# The class that has this specified needs to have a +position+ column defined as an integer on
|
45
|
+
# the mapped database table.
|
46
|
+
#
|
47
|
+
# Todo list example:
|
48
|
+
#
|
49
|
+
# class TodoList < ActiveRecord::Base
|
50
|
+
# has_many :todo_items, order: "position"
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# class TodoItem < ActiveRecord::Base
|
54
|
+
# belongs_to :todo_list
|
55
|
+
# acts_as_list scope: :todo_list
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# todo_list.first.move_to_bottom
|
59
|
+
# todo_list.last.move_higher
|
130
60
|
|
131
|
-
|
61
|
+
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
|
62
|
+
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
|
63
|
+
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
|
64
|
+
# the first in the list of all chapters.
|
132
65
|
end
|
133
66
|
|
134
|
-
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
|
135
|
-
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
|
136
|
-
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
|
137
|
-
# the first in the list of all chapters.
|
138
67
|
module InstanceMethods
|
68
|
+
# Get the current position of the item in the list
|
69
|
+
def current_position
|
70
|
+
position = send(position_column)
|
71
|
+
position ? position.to_i : nil
|
72
|
+
end
|
73
|
+
|
139
74
|
# Insert the item at the given position (defaults to the top position of 1).
|
140
75
|
def insert_at(position = acts_as_list_top)
|
141
76
|
insert_at_position(position)
|
142
77
|
end
|
143
78
|
|
79
|
+
def insert_at!(position = acts_as_list_top)
|
80
|
+
insert_at_position(position, true)
|
81
|
+
end
|
82
|
+
|
144
83
|
# Swap positions with the next lower item, if one exists.
|
145
84
|
def move_lower
|
146
85
|
return unless lower_item
|
147
86
|
|
148
87
|
acts_as_list_class.transaction do
|
149
|
-
lower_item.
|
150
|
-
|
88
|
+
if lower_item.current_position != current_position
|
89
|
+
swap_positions_with(lower_item)
|
90
|
+
else
|
91
|
+
lower_item.decrement_position
|
92
|
+
increment_position
|
93
|
+
end
|
151
94
|
end
|
152
95
|
end
|
153
96
|
|
@@ -156,8 +99,12 @@ module ActiveRecord
|
|
156
99
|
return unless higher_item
|
157
100
|
|
158
101
|
acts_as_list_class.transaction do
|
159
|
-
higher_item.
|
160
|
-
|
102
|
+
if higher_item.current_position != current_position
|
103
|
+
swap_positions_with(higher_item)
|
104
|
+
else
|
105
|
+
higher_item.increment_position
|
106
|
+
decrement_position
|
107
|
+
end
|
161
108
|
end
|
162
109
|
end
|
163
110
|
|
@@ -165,20 +112,14 @@ module ActiveRecord
|
|
165
112
|
# position adjusted accordingly.
|
166
113
|
def move_to_bottom
|
167
114
|
return unless in_list?
|
168
|
-
|
169
|
-
decrement_positions_on_lower_items
|
170
|
-
assume_bottom_position
|
171
|
-
end
|
115
|
+
insert_at_position bottom_position_in_list.to_i
|
172
116
|
end
|
173
117
|
|
174
118
|
# Move to the top of the list. If the item is already in the list, the items above it have their
|
175
119
|
# position adjusted accordingly.
|
176
120
|
def move_to_top
|
177
121
|
return unless in_list?
|
178
|
-
|
179
|
-
increment_positions_on_higher_items
|
180
|
-
assume_top_position
|
181
|
-
end
|
122
|
+
insert_at_position acts_as_list_top
|
182
123
|
end
|
183
124
|
|
184
125
|
# Removes the item from the list.
|
@@ -199,25 +140,23 @@ module ActiveRecord
|
|
199
140
|
# Increase the position of this item without adjusting the rest of the list.
|
200
141
|
def increment_position
|
201
142
|
return unless in_list?
|
202
|
-
set_list_position(
|
143
|
+
set_list_position(current_position + 1)
|
203
144
|
end
|
204
145
|
|
205
146
|
# Decrease the position of this item without adjusting the rest of the list.
|
206
147
|
def decrement_position
|
207
148
|
return unless in_list?
|
208
|
-
set_list_position(
|
149
|
+
set_list_position(current_position - 1)
|
209
150
|
end
|
210
151
|
|
211
|
-
# Return +true+ if this object is the first in the list.
|
212
152
|
def first?
|
213
153
|
return false unless in_list?
|
214
|
-
|
154
|
+
!higher_items(1).exists?
|
215
155
|
end
|
216
156
|
|
217
|
-
# Return +true+ if this object is the last in the list.
|
218
157
|
def last?
|
219
158
|
return false unless in_list?
|
220
|
-
|
159
|
+
!lower_items(1).exists?
|
221
160
|
end
|
222
161
|
|
223
162
|
# Return the next higher item in the list.
|
@@ -230,12 +169,11 @@ module ActiveRecord
|
|
230
169
|
# selects all higher items by default
|
231
170
|
def higher_items(limit=nil)
|
232
171
|
limit ||= acts_as_list_list.count
|
233
|
-
position_value = send(position_column)
|
234
172
|
acts_as_list_list.
|
235
|
-
where("#{
|
236
|
-
where("#{
|
237
|
-
|
238
|
-
|
173
|
+
where("#{quoted_position_column_with_table_name} <= ?", current_position).
|
174
|
+
where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
|
175
|
+
reorder(acts_as_list_order_argument(:desc)).
|
176
|
+
limit(limit)
|
239
177
|
end
|
240
178
|
|
241
179
|
# Return the next lower item in the list.
|
@@ -248,12 +186,11 @@ module ActiveRecord
|
|
248
186
|
# selects all lower items by default
|
249
187
|
def lower_items(limit=nil)
|
250
188
|
limit ||= acts_as_list_list.count
|
251
|
-
position_value = send(position_column)
|
252
189
|
acts_as_list_list.
|
253
|
-
where("#{
|
254
|
-
where("#{
|
255
|
-
|
256
|
-
|
190
|
+
where("#{quoted_position_column_with_table_name} >= ?", current_position).
|
191
|
+
where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
|
192
|
+
reorder(acts_as_list_order_argument(:asc)).
|
193
|
+
limit(limit)
|
257
194
|
end
|
258
195
|
|
259
196
|
# Test if this record is in a list
|
@@ -262,233 +199,289 @@ module ActiveRecord
|
|
262
199
|
end
|
263
200
|
|
264
201
|
def not_in_list?
|
265
|
-
|
202
|
+
current_position.nil?
|
266
203
|
end
|
267
204
|
|
268
205
|
def default_position
|
269
|
-
acts_as_list_class.
|
206
|
+
acts_as_list_class.column_defaults[position_column.to_s]
|
270
207
|
end
|
271
208
|
|
272
209
|
def default_position?
|
273
|
-
default_position && default_position
|
210
|
+
default_position && default_position == current_position
|
274
211
|
end
|
275
212
|
|
276
213
|
# Sets the new position and saves it
|
277
|
-
def set_list_position(new_position)
|
278
|
-
|
279
|
-
save
|
214
|
+
def set_list_position(new_position, raise_exception_if_save_fails=false)
|
215
|
+
self[position_column] = new_position
|
216
|
+
raise_exception_if_save_fails ? save! : save
|
280
217
|
end
|
281
218
|
|
282
219
|
private
|
283
|
-
def acts_as_list_list
|
284
|
-
acts_as_list_class.unscoped do
|
285
|
-
acts_as_list_class.where(scope_condition)
|
286
|
-
end
|
287
|
-
end
|
288
220
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
221
|
+
def swap_positions_with(item)
|
222
|
+
item_position = item.current_position
|
223
|
+
|
224
|
+
item.set_list_position(current_position)
|
225
|
+
set_list_position(item_position)
|
226
|
+
end
|
227
|
+
|
228
|
+
def acts_as_list_list
|
229
|
+
acts_as_list_class.default_scoped.unscope(:select, :where).where(scope_condition)
|
230
|
+
end
|
297
231
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
if
|
232
|
+
def avoid_collision
|
233
|
+
case add_new_at
|
234
|
+
when :top
|
235
|
+
if assume_default_position?
|
236
|
+
increment_positions_on_all_items
|
237
|
+
self[position_column] = acts_as_list_top
|
238
|
+
else
|
239
|
+
increment_positions_on_lower_items(self[position_column], id)
|
240
|
+
end
|
241
|
+
when :bottom
|
242
|
+
if assume_default_position?
|
302
243
|
self[position_column] = bottom_position_in_list.to_i + 1
|
303
244
|
else
|
304
245
|
increment_positions_on_lower_items(self[position_column], id)
|
305
246
|
end
|
247
|
+
else
|
248
|
+
increment_positions_on_lower_items(self[position_column], id) if position_changed
|
249
|
+
end
|
306
250
|
|
307
|
-
|
308
|
-
|
251
|
+
@scope_changed = false # Make sure we know that we've processed this scope change already
|
252
|
+
return true # Don't halt the callback chain
|
253
|
+
end
|
309
254
|
|
310
|
-
|
311
|
-
|
312
|
-
|
255
|
+
def assume_default_position?
|
256
|
+
not_in_list? ||
|
257
|
+
persisted? && internal_scope_changed? && !position_changed ||
|
258
|
+
default_position?
|
259
|
+
end
|
313
260
|
|
314
|
-
|
315
|
-
|
261
|
+
# Overwrite this method to define the scope of the list changes
|
262
|
+
def scope_condition() {} end
|
316
263
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
264
|
+
# Returns the bottom position number in the list.
|
265
|
+
# bottom_position_in_list # => 2
|
266
|
+
def bottom_position_in_list(except = nil)
|
267
|
+
item = bottom_item(except)
|
268
|
+
item ? item.current_position : acts_as_list_top - 1
|
269
|
+
end
|
323
270
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
conditions = except ? "#{self.class.primary_key} != #{self.class.connection.quote(except.id)}" : {}
|
328
|
-
acts_as_list_list.in_list.where(
|
329
|
-
conditions
|
330
|
-
).order(
|
331
|
-
"#{acts_as_list_class.table_name}.#{position_column} DESC"
|
332
|
-
).first
|
333
|
-
end
|
271
|
+
# Returns the bottom item
|
272
|
+
def bottom_item(except = nil)
|
273
|
+
scope = acts_as_list_list
|
334
274
|
|
335
|
-
|
336
|
-
|
337
|
-
set_list_position(bottom_position_in_list(self).to_i + 1)
|
275
|
+
if except
|
276
|
+
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", except.id)
|
338
277
|
end
|
339
278
|
|
340
|
-
|
341
|
-
|
342
|
-
set_list_position(acts_as_list_top)
|
343
|
-
end
|
279
|
+
scope.in_list.reorder(acts_as_list_order_argument(:desc)).first
|
280
|
+
end
|
344
281
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
282
|
+
# Forces item to assume the bottom position in the list.
|
283
|
+
def assume_bottom_position
|
284
|
+
set_list_position(bottom_position_in_list(self).to_i + 1)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Forces item to assume the top position in the list.
|
288
|
+
def assume_top_position
|
289
|
+
set_list_position(acts_as_list_top)
|
290
|
+
end
|
291
|
+
|
292
|
+
# This has the effect of moving all the higher items down one.
|
293
|
+
def increment_positions_on_higher_items
|
294
|
+
return unless in_list?
|
295
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", current_position).increment_all
|
296
|
+
end
|
297
|
+
|
298
|
+
# This has the effect of moving all the lower items down one.
|
299
|
+
def increment_positions_on_lower_items(position, avoid_id = nil)
|
300
|
+
scope = acts_as_list_list
|
301
|
+
|
302
|
+
if avoid_id
|
303
|
+
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
|
352
304
|
end
|
353
305
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
acts_as_list_list.where(
|
359
|
-
"#{position_column} > #{position}"
|
360
|
-
).update_all(
|
361
|
-
"#{position_column} = (#{position_column} - 1)"
|
362
|
-
)
|
306
|
+
if sequential_updates?
|
307
|
+
scope.where("#{quoted_position_column_with_table_name} >= ?", position).reorder(acts_as_list_order_argument(:desc)).increment_sequentially
|
308
|
+
else
|
309
|
+
scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
|
363
310
|
end
|
311
|
+
end
|
364
312
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
313
|
+
# This has the effect of moving all the higher items up one.
|
314
|
+
def decrement_positions_on_higher_items(position)
|
315
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} <= ?", position).decrement_all
|
316
|
+
end
|
317
|
+
|
318
|
+
# This has the effect of moving all the lower items up one.
|
319
|
+
def decrement_positions_on_lower_items(position=current_position)
|
320
|
+
return unless in_list?
|
321
|
+
|
322
|
+
if sequential_updates?
|
323
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
|
324
|
+
else
|
325
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
|
373
326
|
end
|
327
|
+
end
|
374
328
|
|
375
|
-
|
376
|
-
|
377
|
-
|
329
|
+
# Increments position (<tt>position_column</tt>) of all items in the list.
|
330
|
+
def increment_positions_on_all_items
|
331
|
+
acts_as_list_list.increment_all
|
332
|
+
end
|
378
333
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
334
|
+
# Reorders intermediate items to support moving an item from old_position to new_position.
|
335
|
+
# unique constraint prevents regular increment_all and forces to do increments one by one
|
336
|
+
# http://stackoverflow.com/questions/7703196/sqlite-increment-unique-integer-field
|
337
|
+
# both SQLite and PostgreSQL (and most probably MySQL too) has same issue
|
338
|
+
# that's why *sequential_updates?* check alters implementation behavior
|
339
|
+
def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
|
340
|
+
return if old_position == new_position
|
341
|
+
scope = acts_as_list_list
|
342
|
+
|
343
|
+
if avoid_id
|
344
|
+
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
|
384
345
|
end
|
385
346
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
347
|
+
if old_position < new_position
|
348
|
+
# Decrement position of intermediate items
|
349
|
+
#
|
350
|
+
# e.g., if moving an item from 2 to 5,
|
351
|
+
# move [3, 4, 5] to [2, 3, 4]
|
352
|
+
items = scope.where(
|
353
|
+
"#{quoted_position_column_with_table_name} > ?", old_position
|
354
|
+
).where(
|
355
|
+
"#{quoted_position_column_with_table_name} <= ?", new_position
|
390
356
|
)
|
391
|
-
end
|
392
357
|
|
393
|
-
|
394
|
-
|
395
|
-
return if old_position == new_position
|
396
|
-
avoid_id_condition = avoid_id ? " AND #{self.class.primary_key} != #{self.class.connection.quote(avoid_id)}" : ''
|
397
|
-
|
398
|
-
if old_position < new_position
|
399
|
-
# Decrement position of intermediate items
|
400
|
-
#
|
401
|
-
# e.g., if moving an item from 2 to 5,
|
402
|
-
# move [3, 4, 5] to [2, 3, 4]
|
403
|
-
acts_as_list_list.where(
|
404
|
-
"#{position_column} > #{old_position}"
|
405
|
-
).where(
|
406
|
-
"#{position_column} <= #{new_position}#{avoid_id_condition}"
|
407
|
-
).update_all(
|
408
|
-
"#{position_column} = (#{position_column} - 1)"
|
409
|
-
)
|
358
|
+
if sequential_updates?
|
359
|
+
items.reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
|
410
360
|
else
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
361
|
+
items.decrement_all
|
362
|
+
end
|
363
|
+
else
|
364
|
+
# Increment position of intermediate items
|
365
|
+
#
|
366
|
+
# e.g., if moving an item from 5 to 2,
|
367
|
+
# move [2, 3, 4] to [3, 4, 5]
|
368
|
+
items = scope.where(
|
369
|
+
"#{quoted_position_column_with_table_name} >= ?", new_position
|
370
|
+
).where(
|
371
|
+
"#{quoted_position_column_with_table_name} < ?", old_position
|
372
|
+
)
|
373
|
+
|
374
|
+
if sequential_updates?
|
375
|
+
items.reorder(acts_as_list_order_argument(:desc)).increment_sequentially
|
376
|
+
else
|
377
|
+
items.increment_all
|
422
378
|
end
|
423
379
|
end
|
380
|
+
end
|
424
381
|
|
425
|
-
|
426
|
-
|
382
|
+
def insert_at_position(position, raise_exception_if_save_fails=false)
|
383
|
+
raise ArgumentError.new("position cannot be lower than top") if position < acts_as_list_top
|
384
|
+
return set_list_position(position, raise_exception_if_save_fails) if new_record?
|
385
|
+
with_lock do
|
427
386
|
if in_list?
|
428
|
-
old_position =
|
387
|
+
old_position = current_position
|
429
388
|
return if position == old_position
|
430
|
-
|
389
|
+
# temporary move after bottom with gap, avoiding duplicate values
|
390
|
+
# gap is required to leave room for position increments
|
391
|
+
# positive number will be valid with unique not null check (>= 0) db constraint
|
392
|
+
temporary_position = bottom_position_in_list + 2
|
393
|
+
set_list_position(temporary_position, raise_exception_if_save_fails)
|
394
|
+
shuffle_positions_on_intermediate_items(old_position, position, id)
|
431
395
|
else
|
432
396
|
increment_positions_on_lower_items(position)
|
433
397
|
end
|
434
|
-
set_list_position(position)
|
398
|
+
set_list_position(position, raise_exception_if_save_fails)
|
435
399
|
end
|
400
|
+
end
|
436
401
|
|
437
|
-
|
438
|
-
|
439
|
-
if in_list?
|
440
|
-
old_position = send(position_column).to_i
|
441
|
-
set_list_position(0)
|
442
|
-
decrement_positions_on_lower_items(old_position)
|
443
|
-
end
|
444
|
-
end
|
402
|
+
def update_positions
|
403
|
+
return unless position_before_save_changed?
|
445
404
|
|
446
|
-
|
447
|
-
old_position = send("#{position_column}_was").to_i
|
448
|
-
new_position = send(position_column).to_i
|
405
|
+
old_position = position_before_save || bottom_position_in_list + 1
|
449
406
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
shuffle_positions_on_intermediate_items old_position, new_position, id
|
454
|
-
end
|
407
|
+
return unless current_position && acts_as_list_list.where(
|
408
|
+
"#{quoted_position_column_with_table_name} = #{current_position}"
|
409
|
+
).count > 1
|
455
410
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
@scope_changed = scope_changed?
|
460
|
-
end
|
411
|
+
shuffle_positions_on_intermediate_items old_position, current_position, id
|
412
|
+
end
|
461
413
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
end
|
468
|
-
end
|
414
|
+
def position_before_save_changed?
|
415
|
+
if active_record_version_is?('>= 5.1')
|
416
|
+
saved_change_to_attribute? position_column
|
417
|
+
else
|
418
|
+
attribute_changed? position_column
|
469
419
|
end
|
420
|
+
end
|
470
421
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
send("add_to_list_#{add_new_at}")
|
477
|
-
end
|
422
|
+
def position_before_save
|
423
|
+
if active_record_version_is?('>= 5.1')
|
424
|
+
attribute_before_last_save position_column
|
425
|
+
else
|
426
|
+
attribute_was position_column
|
478
427
|
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def internal_scope_changed?
|
431
|
+
return @scope_changed if defined?(@scope_changed)
|
432
|
+
|
433
|
+
@scope_changed = scope_changed?
|
434
|
+
end
|
435
|
+
|
436
|
+
def clear_scope_changed
|
437
|
+
remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
|
438
|
+
end
|
439
|
+
|
440
|
+
def check_scope
|
441
|
+
if internal_scope_changed?
|
442
|
+
cached_changes = changes
|
443
|
+
|
444
|
+
cached_changes.each { |attribute, values| send("#{attribute}=", values[0]) }
|
445
|
+
send('decrement_positions_on_lower_items') if lower_item
|
446
|
+
cached_changes.each { |attribute, values| send("#{attribute}=", values[1]) }
|
479
447
|
|
480
|
-
|
481
|
-
self.reload
|
448
|
+
avoid_collision
|
482
449
|
end
|
450
|
+
end
|
483
451
|
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
end
|
452
|
+
# This check is skipped if the position is currently the default position from the table
|
453
|
+
# as modifying the default position on creation is handled elsewhere
|
454
|
+
def check_top_position
|
455
|
+
if current_position && !default_position? && current_position < acts_as_list_top
|
456
|
+
self[position_column] = acts_as_list_top
|
490
457
|
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)
|
461
|
+
def quoted_position_column
|
462
|
+
@_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
|
463
|
+
end
|
464
|
+
|
465
|
+
# Used in order clauses
|
466
|
+
def quoted_table_name
|
467
|
+
@_quoted_table_name ||= acts_as_list_class.quoted_table_name
|
468
|
+
end
|
469
|
+
|
470
|
+
def quoted_position_column_with_table_name
|
471
|
+
@_quoted_position_column_with_table_name ||= "#{quoted_table_name}.#{quoted_position_column}"
|
472
|
+
end
|
473
|
+
|
474
|
+
def acts_as_list_order_argument(direction = :asc)
|
475
|
+
{ position_column => direction }
|
476
|
+
end
|
477
|
+
|
478
|
+
def active_record_version_is?(version_requirement)
|
479
|
+
requirement = Gem::Requirement.new(version_requirement)
|
480
|
+
version = Gem.loaded_specs['activerecord'].version
|
481
|
+
requirement.satisfied_by?(version)
|
482
|
+
end
|
491
483
|
end
|
484
|
+
|
492
485
|
end
|
493
486
|
end
|
494
487
|
end
|