acts_as_list 0.7.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +1 -0
- data/.travis.yml +23 -3
- data/Appraisals +13 -3
- data/CHANGELOG.md +131 -2
- data/Gemfile +18 -11
- data/README.md +49 -6
- data/gemfiles/rails_3_2.gemfile +16 -7
- data/gemfiles/rails_4_1.gemfile +16 -7
- data/gemfiles/rails_4_2.gemfile +16 -7
- data/gemfiles/rails_5_0.gemfile +32 -0
- data/lib/acts_as_list/active_record/acts/add_new_at_method_definer.rb +9 -0
- data/lib/acts_as_list/active_record/acts/aux_method_definer.rb +9 -0
- data/lib/acts_as_list/active_record/acts/callback_definer.rb +19 -0
- data/lib/acts_as_list/active_record/acts/column_method_definer.rb +50 -0
- data/lib/acts_as_list/active_record/acts/list.rb +247 -291
- data/lib/acts_as_list/active_record/acts/no_update.rb +50 -0
- data/lib/acts_as_list/active_record/acts/scope_method_definer.rb +49 -0
- data/lib/acts_as_list/active_record/acts/sequential_updates_method_definer.rb +21 -0
- data/lib/acts_as_list/active_record/acts/top_of_list_method_definer.rb +13 -0
- data/lib/acts_as_list/version.rb +1 -1
- data/lib/acts_as_list.rb +8 -14
- data/test/database.yml +16 -0
- data/test/helper.rb +14 -2
- data/test/shared.rb +1 -0
- data/test/shared_array_scope_list.rb +19 -4
- data/test/shared_list.rb +44 -8
- data/test/shared_list_sub.rb +61 -2
- data/test/shared_no_addition.rb +13 -2
- data/test/shared_quoting.rb +21 -0
- data/test/shared_top_addition.rb +34 -13
- data/test/shared_zero_based.rb +11 -0
- data/test/test_joined_list.rb +70 -0
- data/test/test_list.rb +307 -28
- metadata +26 -11
@@ -1,10 +1,42 @@
|
|
1
|
+
class << ActiveRecord::Base
|
2
|
+
# Configuration options are:
|
3
|
+
#
|
4
|
+
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
|
5
|
+
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
|
6
|
+
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
|
7
|
+
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
|
8
|
+
# Example: <tt>acts_as_list scope: 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
|
9
|
+
# * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
|
10
|
+
# act more like an array in its indexing.
|
11
|
+
# * +add_new_at+ - specifies whether objects get added to the :top or :bottom of the list. (default: +bottom+)
|
12
|
+
# `nil` will result in new items not being added to the list on create.
|
13
|
+
# * +sequential_updates+ - specifies whether insert_at should update objects positions during shuffling
|
14
|
+
# one by one to respect position column unique not null constraint.
|
15
|
+
# Defaults to true if position column has unique index, otherwise false.
|
16
|
+
# If constraint is <tt>deferrable initially deferred<tt>, overriding it with false will speed up insert_at.
|
17
|
+
def acts_as_list(options = {})
|
18
|
+
configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom }
|
19
|
+
configuration.update(options) if options.is_a?(Hash)
|
20
|
+
|
21
|
+
caller_class = self
|
22
|
+
|
23
|
+
ActiveRecord::Acts::List::ColumnMethodDefiner.call(caller_class, configuration[:column])
|
24
|
+
ActiveRecord::Acts::List::ScopeMethodDefiner.call(caller_class, configuration[:scope])
|
25
|
+
ActiveRecord::Acts::List::TopOfListMethodDefiner.call(caller_class, configuration[:top_of_list])
|
26
|
+
ActiveRecord::Acts::List::AddNewAtMethodDefiner.call(caller_class, configuration[:add_new_at])
|
27
|
+
|
28
|
+
ActiveRecord::Acts::List::AuxMethodDefiner.call(caller_class)
|
29
|
+
ActiveRecord::Acts::List::CallbackDefiner.call(caller_class, configuration[:add_new_at])
|
30
|
+
ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner.call(caller_class, configuration[:column], configuration[:sequential_updates])
|
31
|
+
|
32
|
+
include ActiveRecord::Acts::List::InstanceMethods
|
33
|
+
include ActiveRecord::Acts::List::NoUpdate
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
1
37
|
module ActiveRecord
|
2
38
|
module Acts #:nodoc:
|
3
39
|
module List #:nodoc:
|
4
|
-
def self.included(base)
|
5
|
-
base.extend(ClassMethods)
|
6
|
-
end
|
7
|
-
|
8
40
|
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
|
9
41
|
# The class that has this specified needs to have a +position+ column defined as an integer on
|
10
42
|
# the mapped database table.
|
@@ -22,114 +54,6 @@ module ActiveRecord
|
|
22
54
|
#
|
23
55
|
# todo_list.first.move_to_bottom
|
24
56
|
# todo_list.last.move_higher
|
25
|
-
module ClassMethods
|
26
|
-
# Configuration options are:
|
27
|
-
#
|
28
|
-
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
|
29
|
-
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
|
30
|
-
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
|
31
|
-
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
|
32
|
-
# Example: <tt>acts_as_list scope: 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
|
33
|
-
# * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
|
34
|
-
# act more like an array in its indexing.
|
35
|
-
# * +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
|
37
|
-
def acts_as_list(options = {})
|
38
|
-
configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom}
|
39
|
-
configuration.update(options) if options.is_a?(Hash)
|
40
|
-
|
41
|
-
if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
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
|
91
|
-
|
92
|
-
def scope_name
|
93
|
-
'#{configuration[:scope]}'
|
94
|
-
end
|
95
|
-
|
96
|
-
def add_new_at
|
97
|
-
'#{configuration[:add_new_at]}'
|
98
|
-
end
|
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
|
113
|
-
|
114
|
-
before_validation :check_top_position
|
115
|
-
|
116
|
-
before_destroy :reload_position
|
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
|
126
|
-
|
127
|
-
if configuration[:add_new_at].present?
|
128
|
-
self.send :before_create, :"add_to_list_#{configuration[:add_new_at]}"
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
132
|
-
end
|
133
57
|
|
134
58
|
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
|
135
59
|
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
|
@@ -146,8 +70,12 @@ module ActiveRecord
|
|
146
70
|
return unless lower_item
|
147
71
|
|
148
72
|
acts_as_list_class.transaction do
|
149
|
-
lower_item.
|
150
|
-
|
73
|
+
if lower_item.send(position_column) != self.send(position_column)
|
74
|
+
swap_positions(lower_item, self)
|
75
|
+
else
|
76
|
+
lower_item.decrement_position
|
77
|
+
increment_position
|
78
|
+
end
|
151
79
|
end
|
152
80
|
end
|
153
81
|
|
@@ -156,8 +84,12 @@ module ActiveRecord
|
|
156
84
|
return unless higher_item
|
157
85
|
|
158
86
|
acts_as_list_class.transaction do
|
159
|
-
higher_item.
|
160
|
-
|
87
|
+
if higher_item.send(position_column) != self.send(position_column)
|
88
|
+
swap_positions(higher_item, self)
|
89
|
+
else
|
90
|
+
higher_item.increment_position
|
91
|
+
decrement_position
|
92
|
+
end
|
161
93
|
end
|
162
94
|
end
|
163
95
|
|
@@ -208,16 +140,14 @@ module ActiveRecord
|
|
208
140
|
set_list_position(self.send(position_column).to_i - 1)
|
209
141
|
end
|
210
142
|
|
211
|
-
# Return +true+ if this object is the first in the list.
|
212
143
|
def first?
|
213
144
|
return false unless in_list?
|
214
|
-
|
145
|
+
!higher_item
|
215
146
|
end
|
216
147
|
|
217
|
-
# Return +true+ if this object is the last in the list.
|
218
148
|
def last?
|
219
149
|
return false unless in_list?
|
220
|
-
|
150
|
+
!lower_item
|
221
151
|
end
|
222
152
|
|
223
153
|
# Return the next higher item in the list.
|
@@ -232,10 +162,10 @@ module ActiveRecord
|
|
232
162
|
limit ||= acts_as_list_list.count
|
233
163
|
position_value = send(position_column)
|
234
164
|
acts_as_list_list.
|
235
|
-
where("#{
|
236
|
-
where("#{
|
237
|
-
|
238
|
-
|
165
|
+
where("#{quoted_position_column_with_table_name} <= ?", position_value).
|
166
|
+
where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
|
167
|
+
order("#{quoted_position_column_with_table_name} DESC").
|
168
|
+
limit(limit)
|
239
169
|
end
|
240
170
|
|
241
171
|
# Return the next lower item in the list.
|
@@ -250,10 +180,10 @@ module ActiveRecord
|
|
250
180
|
limit ||= acts_as_list_list.count
|
251
181
|
position_value = send(position_column)
|
252
182
|
acts_as_list_list.
|
253
|
-
where("#{
|
254
|
-
where("#{
|
255
|
-
|
256
|
-
|
183
|
+
where("#{quoted_position_column_with_table_name} >= ?", position_value).
|
184
|
+
where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
|
185
|
+
order("#{quoted_position_column_with_table_name} ASC").
|
186
|
+
limit(limit)
|
257
187
|
end
|
258
188
|
|
259
189
|
# Test if this record is in a list
|
@@ -280,214 +210,240 @@ module ActiveRecord
|
|
280
210
|
end
|
281
211
|
|
282
212
|
private
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
213
|
+
|
214
|
+
def swap_positions(item1, item2)
|
215
|
+
item1_position = item1.send(position_column)
|
216
|
+
|
217
|
+
item1.set_list_position(item2.send(position_column))
|
218
|
+
item2.set_list_position(item1_position)
|
219
|
+
end
|
220
|
+
|
221
|
+
def acts_as_list_list
|
222
|
+
acts_as_list_class.unscoped do
|
223
|
+
acts_as_list_class.where(scope_condition)
|
287
224
|
end
|
225
|
+
end
|
288
226
|
|
289
|
-
|
227
|
+
# Poorly named methods. They will insert the item at the desired position if the position
|
228
|
+
# has been set manually using position=, not necessarily the top or bottom of the list:
|
229
|
+
|
230
|
+
def add_to_list_top
|
231
|
+
if not_in_list? || internal_scope_changed? && !position_changed || default_position?
|
290
232
|
increment_positions_on_all_items
|
291
233
|
self[position_column] = acts_as_list_top
|
292
|
-
|
293
|
-
|
294
|
-
#dont halt the callback chain
|
295
|
-
true
|
234
|
+
else
|
235
|
+
increment_positions_on_lower_items(self[position_column], id)
|
296
236
|
end
|
297
237
|
|
298
|
-
#
|
299
|
-
|
300
|
-
def add_to_list_bottom
|
301
|
-
if not_in_list? || internal_scope_changed? && !@position_changed || default_position?
|
302
|
-
self[position_column] = bottom_position_in_list.to_i + 1
|
303
|
-
else
|
304
|
-
increment_positions_on_lower_items(self[position_column], id)
|
305
|
-
end
|
238
|
+
# Make sure we know that we've processed this scope change already
|
239
|
+
@scope_changed = false
|
306
240
|
|
307
|
-
|
308
|
-
|
241
|
+
# Don't halt the callback chain
|
242
|
+
true
|
243
|
+
end
|
309
244
|
|
310
|
-
|
311
|
-
|
245
|
+
def add_to_list_bottom
|
246
|
+
if not_in_list? || internal_scope_changed? && !position_changed || default_position?
|
247
|
+
self[position_column] = bottom_position_in_list.to_i + 1
|
248
|
+
else
|
249
|
+
increment_positions_on_lower_items(self[position_column], id)
|
312
250
|
end
|
313
251
|
|
314
|
-
#
|
315
|
-
|
252
|
+
# Make sure we know that we've processed this scope change already
|
253
|
+
@scope_changed = false
|
316
254
|
|
317
|
-
#
|
318
|
-
|
319
|
-
|
320
|
-
item = bottom_item(except)
|
321
|
-
item ? item.send(position_column) : acts_as_list_top - 1
|
322
|
-
end
|
255
|
+
# Don't halt the callback chain
|
256
|
+
true
|
257
|
+
end
|
323
258
|
|
324
|
-
|
325
|
-
|
326
|
-
conditions = scope_condition
|
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
|
259
|
+
# Overwrite this method to define the scope of the list changes
|
260
|
+
def scope_condition() {} end
|
334
261
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
262
|
+
# Returns the bottom position number in the list.
|
263
|
+
# bottom_position_in_list # => 2
|
264
|
+
def bottom_position_in_list(except = nil)
|
265
|
+
item = bottom_item(except)
|
266
|
+
item ? item.send(position_column) : acts_as_list_top - 1
|
267
|
+
end
|
339
268
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
269
|
+
# Returns the bottom item
|
270
|
+
def bottom_item(except = nil)
|
271
|
+
conditions = except ? "#{quoted_table_name}.#{self.class.primary_key} != #{self.class.connection.quote(except.id)}" : {}
|
272
|
+
acts_as_list_list.in_list.where(
|
273
|
+
conditions
|
274
|
+
).order(
|
275
|
+
"#{quoted_position_column_with_table_name} DESC"
|
276
|
+
).first
|
277
|
+
end
|
344
278
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
).update_all(
|
350
|
-
"#{position_column} = (#{position_column} - 1)"
|
351
|
-
)
|
352
|
-
end
|
279
|
+
# Forces item to assume the bottom position in the list.
|
280
|
+
def assume_bottom_position
|
281
|
+
set_list_position(bottom_position_in_list(self).to_i + 1)
|
282
|
+
end
|
353
283
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
acts_as_list_list.where(
|
359
|
-
"#{position_column} > #{position}"
|
360
|
-
).update_all(
|
361
|
-
"#{position_column} = (#{position_column} - 1)"
|
362
|
-
)
|
363
|
-
end
|
284
|
+
# Forces item to assume the top position in the list.
|
285
|
+
def assume_top_position
|
286
|
+
set_list_position(acts_as_list_top)
|
287
|
+
end
|
364
288
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
289
|
+
# This has the effect of moving all the higher items down one.
|
290
|
+
def increment_positions_on_higher_items
|
291
|
+
return unless in_list?
|
292
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", send(position_column).to_i).increment_all
|
293
|
+
end
|
294
|
+
|
295
|
+
# This has the effect of moving all the lower items down one.
|
296
|
+
def increment_positions_on_lower_items(position, avoid_id = nil)
|
297
|
+
scope = acts_as_list_list
|
298
|
+
|
299
|
+
if avoid_id
|
300
|
+
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.class.connection.quote(avoid_id))
|
373
301
|
end
|
374
302
|
|
375
|
-
#
|
376
|
-
|
377
|
-
avoid_id_condition = avoid_id ? " AND #{self.class.primary_key} != #{self.class.connection.quote(avoid_id)}" : ''
|
303
|
+
scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
|
304
|
+
end
|
378
305
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
306
|
+
# This has the effect of moving all the higher items up one.
|
307
|
+
def decrement_positions_on_higher_items(position)
|
308
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} <= ?", position).decrement_all
|
309
|
+
end
|
310
|
+
|
311
|
+
# This has the effect of moving all the lower items up one.
|
312
|
+
def decrement_positions_on_lower_items(position=nil)
|
313
|
+
return unless in_list?
|
314
|
+
position ||= send(position_column).to_i
|
315
|
+
acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
|
316
|
+
end
|
317
|
+
|
318
|
+
# Increments position (<tt>position_column</tt>) of all items in the list.
|
319
|
+
def increment_positions_on_all_items
|
320
|
+
acts_as_list_list.increment_all
|
321
|
+
end
|
322
|
+
|
323
|
+
# Reorders intermediate items to support moving an item from old_position to new_position.
|
324
|
+
# unique constraint prevents regular increment_all and forces to do increments one by one
|
325
|
+
# http://stackoverflow.com/questions/7703196/sqlite-increment-unique-integer-field
|
326
|
+
# both SQLite and PostgreSQL (and most probably MySQL too) has same issue
|
327
|
+
# that's why *sequential_updates?* check alters implementation behavior
|
328
|
+
def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
|
329
|
+
return if old_position == new_position
|
330
|
+
scope = acts_as_list_list
|
331
|
+
|
332
|
+
if avoid_id
|
333
|
+
scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.class.connection.quote(avoid_id))
|
384
334
|
end
|
385
335
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
336
|
+
if old_position < new_position
|
337
|
+
# Decrement position of intermediate items
|
338
|
+
#
|
339
|
+
# e.g., if moving an item from 2 to 5,
|
340
|
+
# move [3, 4, 5] to [2, 3, 4]
|
341
|
+
items = scope.where(
|
342
|
+
"#{quoted_position_column_with_table_name} > ?", old_position
|
343
|
+
).where(
|
344
|
+
"#{quoted_position_column_with_table_name} <= ?", new_position
|
390
345
|
)
|
391
|
-
end
|
392
346
|
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
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
|
-
)
|
347
|
+
if sequential_updates?
|
348
|
+
items.order("#{quoted_position_column_with_table_name} ASC").each do |item|
|
349
|
+
item.decrement!(position_column)
|
350
|
+
end
|
410
351
|
else
|
411
|
-
|
412
|
-
#
|
413
|
-
# e.g., if moving an item from 5 to 2,
|
414
|
-
# move [2, 3, 4] to [3, 4, 5]
|
415
|
-
acts_as_list_list.where(
|
416
|
-
"#{position_column} >= #{new_position}"
|
417
|
-
).where(
|
418
|
-
"#{position_column} < #{old_position}#{avoid_id_condition}"
|
419
|
-
).update_all(
|
420
|
-
"#{position_column} = (#{position_column} + 1)"
|
421
|
-
)
|
352
|
+
items.decrement_all
|
422
353
|
end
|
423
|
-
|
354
|
+
else
|
355
|
+
# Increment position of intermediate items
|
356
|
+
#
|
357
|
+
# e.g., if moving an item from 5 to 2,
|
358
|
+
# move [2, 3, 4] to [3, 4, 5]
|
359
|
+
items = scope.where(
|
360
|
+
"#{quoted_position_column_with_table_name} >= ?", new_position
|
361
|
+
).where(
|
362
|
+
"#{quoted_position_column_with_table_name} < ?", old_position
|
363
|
+
)
|
424
364
|
|
425
|
-
|
426
|
-
|
365
|
+
if sequential_updates?
|
366
|
+
items.order("#{quoted_position_column_with_table_name} DESC").each do |item|
|
367
|
+
item.increment!(position_column)
|
368
|
+
end
|
369
|
+
else
|
370
|
+
items.increment_all
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def insert_at_position(position)
|
376
|
+
return set_list_position(position) if new_record?
|
377
|
+
with_lock do
|
427
378
|
if in_list?
|
428
379
|
old_position = send(position_column).to_i
|
429
380
|
return if position == old_position
|
430
|
-
|
381
|
+
# temporary move after bottom with gap, avoiding duplicate values
|
382
|
+
# gap is required to leave room for position increments
|
383
|
+
# positive number will be valid with unique not null check (>= 0) db constraint
|
384
|
+
temporary_position = acts_as_list_class.maximum(position_column).to_i + 2
|
385
|
+
set_list_position(temporary_position)
|
386
|
+
shuffle_positions_on_intermediate_items(old_position, position, id)
|
431
387
|
else
|
432
388
|
increment_positions_on_lower_items(position)
|
433
389
|
end
|
434
390
|
set_list_position(position)
|
435
391
|
end
|
392
|
+
end
|
436
393
|
|
437
|
-
|
438
|
-
|
439
|
-
|
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
|
394
|
+
def update_positions
|
395
|
+
old_position = send("#{position_column}_was") || bottom_position_in_list + 1
|
396
|
+
new_position = send(position_column).to_i
|
445
397
|
|
446
|
-
|
447
|
-
|
448
|
-
|
398
|
+
return unless acts_as_list_list.where(
|
399
|
+
"#{quoted_position_column_with_table_name} = #{new_position}"
|
400
|
+
).count > 1
|
401
|
+
shuffle_positions_on_intermediate_items old_position, new_position, id
|
402
|
+
end
|
449
403
|
|
450
|
-
|
451
|
-
|
452
|
-
).count > 1
|
453
|
-
shuffle_positions_on_intermediate_items old_position, new_position, id
|
454
|
-
end
|
404
|
+
def internal_scope_changed?
|
405
|
+
return @scope_changed if defined?(@scope_changed)
|
455
406
|
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
@scope_changed = scope_changed?
|
460
|
-
end
|
407
|
+
@scope_changed = scope_changed?
|
408
|
+
end
|
461
409
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
if self.class.column_names.include? k
|
466
|
-
@changed_attributes[k], self[k] = self[k], @changed_attributes[k]
|
467
|
-
end
|
468
|
-
end
|
469
|
-
end
|
410
|
+
def clear_scope_changed
|
411
|
+
remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
|
412
|
+
end
|
470
413
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
send('decrement_positions_on_lower_items') if lower_item
|
475
|
-
swap_changed_attributes
|
476
|
-
send("add_to_list_#{add_new_at}")
|
477
|
-
end
|
478
|
-
end
|
414
|
+
def check_scope
|
415
|
+
if internal_scope_changed?
|
416
|
+
cached_changes = changes
|
479
417
|
|
480
|
-
|
481
|
-
|
418
|
+
cached_changes.each { |attribute, values| self[attribute] = values[0] }
|
419
|
+
send('decrement_positions_on_lower_items') if lower_item
|
420
|
+
cached_changes.each { |attribute, values| self[attribute] = values[1] }
|
421
|
+
|
422
|
+
send("add_to_list_#{add_new_at}") if add_new_at.present?
|
482
423
|
end
|
424
|
+
end
|
483
425
|
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
end
|
426
|
+
# This check is skipped if the position is currently the default position from the table
|
427
|
+
# as modifying the default position on creation is handled elsewhere
|
428
|
+
def check_top_position
|
429
|
+
if send(position_column) && !default_position? && send(position_column) < acts_as_list_top
|
430
|
+
self[position_column] = acts_as_list_top
|
490
431
|
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)
|
435
|
+
def quoted_position_column
|
436
|
+
@_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
|
437
|
+
end
|
438
|
+
|
439
|
+
# Used in order clauses
|
440
|
+
def quoted_table_name
|
441
|
+
@_quoted_table_name ||= acts_as_list_class.quoted_table_name
|
442
|
+
end
|
443
|
+
|
444
|
+
def quoted_position_column_with_table_name
|
445
|
+
@_quoted_position_column_with_table_name ||= "#{quoted_table_name}.#{quoted_position_column}"
|
446
|
+
end
|
491
447
|
end
|
492
448
|
end
|
493
449
|
end
|