acts_as_list 0.7.4 → 0.9.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/.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
|