sortifiable 0.1.2 → 0.2.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.
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ *0.2.0 (April 7th, 2011)
2
+
3
+ * Use pessimistic locking to reduce concurrency problems
4
+
5
+ * Use update_all instead of update_attribute so callbacks are not triggered
6
+
7
+
1
8
  *0.1.2 (February 7th, 2011)
2
9
 
3
10
  * Configure association scopes correctly
data/lib/sortifiable.rb CHANGED
@@ -70,7 +70,7 @@ module Sortifiable
70
70
  options[:class] = self
71
71
 
72
72
  include InstanceMethods
73
- before_create :add_to_list_bottom
73
+ before_create :add_to_list
74
74
  before_destroy :decrement_position_on_lower_items, :if => :in_list?
75
75
 
76
76
  self.acts_as_list_options = options
@@ -85,9 +85,18 @@ module Sortifiable
85
85
  module InstanceMethods
86
86
  # Add the item to the end of the list
87
87
  def add_to_list
88
- list_class.transaction do
89
- remove_from_list if in_list?
90
- update_attribute(position_column, last_position + 1)
88
+ if in_list?
89
+ move_to_bottom
90
+ else
91
+ list_class.transaction do
92
+ ids = lock_list!
93
+ last_position = ids.size
94
+ if persisted?
95
+ update_position(last_position + 1)
96
+ else
97
+ send("#{position_column}=".to_sym, last_position + 1)
98
+ end
99
+ end
91
100
  end
92
101
  end
93
102
 
@@ -98,7 +107,7 @@ module Sortifiable
98
107
 
99
108
  # Decrease the position of this item without adjusting the rest of the list.
100
109
  def decrement_position
101
- in_list? && update_attribute(position_column, current_position - 1)
110
+ in_list? && update_position(current_position - 1)
102
111
  end
103
112
 
104
113
  # Return +true+ if this object is the first in the list.
@@ -131,23 +140,44 @@ module Sortifiable
131
140
 
132
141
  # Increase the position of this item without adjusting the rest of the list.
133
142
  def increment_position
134
- in_list? && update_attribute(position_column, current_position + 1)
143
+ in_list? && update_position(current_position + 1)
135
144
  end
136
145
 
137
146
  # Insert the item at the given position (defaults to the top position of 1).
138
147
  def insert_at(position = 1)
139
- if position > 0
140
- list_class.transaction do
141
- remove_from_list
142
- if position > last_position
143
- add_to_list
144
- else
145
- increment_position_on_lower_items(position - 1)
146
- update_attribute(position_column, position)
147
- end
148
+ list_class.transaction do
149
+ ids = lock_list!
150
+ position = [[1, position].max, ids.size].min
151
+
152
+ if persisted?
153
+ current_position = ids.index(id) + 1
154
+
155
+ sql = <<-SQL
156
+ #{quoted_position_column} = CASE
157
+ WHEN #{quoted_position_column} = #{current_position} THEN #{position}
158
+ WHEN #{quoted_position_column} > #{current_position}
159
+ AND #{quoted_position_column} < #{position} THEN #{quoted_position_column} - 1
160
+ WHEN #{quoted_position_column} < #{current_position}
161
+ AND #{quoted_position_column} >= #{position} THEN #{quoted_position_column} + 1
162
+ ELSE #{quoted_position_column}
163
+ END
164
+ SQL
165
+
166
+ list_scope.update_all(sql)
167
+ update_position(position)
168
+ else
169
+ save!
170
+
171
+ sql = <<-SQL
172
+ #{quoted_position_column} = CASE
173
+ WHEN #{quoted_position_column} >= #{position} THEN #{quoted_position_column} + 1
174
+ ELSE #{quoted_position_column}
175
+ END
176
+ SQL
177
+
178
+ list_scope.update_all(sql)
179
+ update_position(position)
148
180
  end
149
- else
150
- false
151
181
  end
152
182
  end
153
183
 
@@ -188,34 +218,125 @@ module Sortifiable
188
218
 
189
219
  # Swap positions with the next higher item, if one exists.
190
220
  def move_higher
191
- in_list? && (first? || insert_at(current_position - 1))
221
+ if in_list?
222
+ list_class.transaction do
223
+ ids = lock_list!
224
+ current_position, last_position = ids.index(id) + 1, ids.size
225
+
226
+ if current_position > 1
227
+ sql = <<-SQL
228
+ #{quoted_position_column} = CASE
229
+ WHEN #{quoted_position_column} = #{current_position} - 1 THEN #{current_position}
230
+ WHEN #{quoted_position_column} = #{current_position} THEN #{current_position} - 1
231
+ ELSE #{quoted_position_column}
232
+ END
233
+ SQL
234
+
235
+ send("#{position_column}=".to_sym, current_position - 1)
236
+ list_scope.update_all(sql) > 0
237
+ end
238
+ end
239
+ else
240
+ false
241
+ end
192
242
  end
193
243
  alias_method :move_up, :move_higher
194
244
 
195
245
  # Swap positions with the next lower item, if one exists.
196
246
  def move_lower
197
- in_list? && (last? || insert_at(current_position + 1))
247
+ if in_list?
248
+ list_class.transaction do
249
+ ids = lock_list!
250
+ current_position, last_position = ids.index(id) + 1, ids.size
251
+
252
+ if current_position < last_position
253
+ sql = <<-SQL
254
+ #{quoted_position_column} = CASE
255
+ WHEN #{quoted_position_column} = #{current_position} + 1 THEN #{current_position}
256
+ WHEN #{quoted_position_column} = #{current_position} THEN #{current_position} + 1
257
+ ELSE #{quoted_position_column}
258
+ END
259
+ SQL
260
+
261
+ send("#{position_column}=".to_sym, current_position + 1)
262
+ list_scope.update_all(sql) > 0
263
+ end
264
+ end
265
+ else
266
+ false
267
+ end
198
268
  end
199
269
  alias_method :move_down, :move_lower
200
270
 
201
271
  # Move to the bottom of the list. If the item is already in the list,
202
272
  # the items below it have their position adjusted accordingly.
203
273
  def move_to_bottom
204
- in_list? && (last? || add_to_list)
274
+ if in_list?
275
+ list_class.transaction do
276
+ ids = lock_list!
277
+ current_position, last_position = ids.index(id) + 1, ids.size
278
+
279
+ if current_position < last_position
280
+ sql = <<-SQL
281
+ #{quoted_position_column} = CASE
282
+ WHEN #{quoted_position_column} = #{current_position} THEN #{last_position}
283
+ WHEN #{quoted_position_column} > #{current_position} THEN #{quoted_position_column} - 1
284
+ ELSE #{quoted_position_column}
285
+ END
286
+ SQL
287
+
288
+ send("#{position_column}=".to_sym, last_position)
289
+ list_scope.update_all(sql) > 0
290
+ end
291
+ end
292
+ else
293
+ false
294
+ end
205
295
  end
206
296
 
207
297
  # Move to the top of the list. If the item is already in the list,
208
298
  # the items above it have their position adjusted accordingly.
209
299
  def move_to_top
210
- in_list? && (first? || insert_at(1))
300
+ if in_list?
301
+ list_class.transaction do
302
+ ids = lock_list!
303
+ current_position, last_position = ids.index(id) + 1, ids.size
304
+
305
+ if current_position > 1
306
+ sql = <<-SQL
307
+ #{quoted_position_column} = CASE
308
+ WHEN #{quoted_position_column} = #{current_position} THEN 1
309
+ WHEN #{quoted_position_column} < #{current_position} THEN #{quoted_position_column} + 1
310
+ ELSE #{quoted_position_column}
311
+ END
312
+ SQL
313
+
314
+ send("#{position_column}=".to_sym, 1)
315
+ list_scope.update_all(sql) > 0
316
+ end
317
+ end
318
+ else
319
+ false
320
+ end
211
321
  end
212
322
 
213
323
  # Removes the item from the list.
214
324
  def remove_from_list
215
325
  if in_list?
216
326
  list_class.transaction do
217
- decrement_position_on_lower_items
218
- update_attribute(position_column, nil)
327
+ ids = lock_list!
328
+ current_position, last_position = ids.index(id) + 1, ids.size
329
+
330
+ sql = <<-SQL
331
+ #{quoted_position_column} = CASE
332
+ WHEN #{quoted_position_column} = #{current_position} THEN NULL
333
+ WHEN #{quoted_position_column} > #{current_position} THEN #{quoted_position_column} - 1
334
+ ELSE #{quoted_position_column}
335
+ END
336
+ SQL
337
+
338
+ send("#{position_column}=".to_sym, nil)
339
+ list_scope.update_all(sql) > 0
219
340
  end
220
341
  else
221
342
  false
@@ -223,20 +344,14 @@ module Sortifiable
223
344
  end
224
345
 
225
346
  private
226
- def add_to_list_bottom #:nodoc:
227
- send("#{position_column}=".to_sym, last_position + 1)
228
- end
229
-
230
347
  def base_scope #:nodoc:
231
348
  list_class.unscoped.where(scope_condition)
232
349
  end
233
350
 
234
351
  def decrement_position_on_lower_items #:nodoc:
235
- lower_scope(current_position).update_all(position_update('- 1'))
236
- end
237
-
238
- def increment_position_on_lower_items(position) #:nodoc:
239
- lower_scope(position).update_all(position_update('+ 1'))
352
+ update = "#{quoted_position_column} = #{quoted_position_column} - 1"
353
+ conditions = "#{quoted_position_column} > #{current_position}"
354
+ list_scope.update_all(update, conditions) > 0
240
355
  end
241
356
 
242
357
  def list_class #:nodoc:
@@ -247,6 +362,10 @@ module Sortifiable
247
362
  base_scope.order(position_column).where("#{quoted_position_column} IS NOT NULL")
248
363
  end
249
364
 
365
+ def lock_list! #:nodoc:
366
+ connection.select_values(list_scope.select(list_class.primary_key).lock(true).to_sql)
367
+ end
368
+
250
369
  def lower_scope(position) #:nodoc:
251
370
  base_scope.where(["#{quoted_position_column} > ?", position])
252
371
  end
@@ -259,10 +378,6 @@ module Sortifiable
259
378
  acts_as_list_options[:column]
260
379
  end
261
380
 
262
- def position_update(direction) #:nodoc:
263
- "#{quoted_position_column} = (#{quoted_position_column} #{direction})"
264
- end
265
-
266
381
  def quoted_position_column #:nodoc:
267
382
  connection.quote_column_name(position_column)
268
383
  end
@@ -274,6 +389,11 @@ module Sortifiable
274
389
  Array.wrap(acts_as_list_options[:scope]).inject({}){ |m,k| m[k] = send(k); m }
275
390
  end
276
391
  end
392
+
393
+ def update_position(new_position)
394
+ list_class.update_all(["#{quoted_position_column} = ?", new_position], list_class.primary_key => id)
395
+ send("#{position_column}=".to_sym, new_position)
396
+ end
277
397
  end
278
398
  end
279
399
 
@@ -1,3 +1,3 @@
1
1
  module Sortifiable
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -129,6 +129,16 @@ class ListTest < Test::Unit::TestCase
129
129
  assert_equal [4, 1, 3, 2], ListMixin.where('parent_id = 5').map(&:id)
130
130
  end
131
131
 
132
+ def test_bounds_checking
133
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:pos)
134
+
135
+ ListMixin.find(1).move_higher
136
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:pos)
137
+
138
+ ListMixin.find(4).move_lower
139
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:pos)
140
+ end
141
+
132
142
  def test_move_to_bottom_with_next_to_last_item
133
143
  assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
134
144
  ListMixin.find(3).move_to_bottom
@@ -345,6 +355,16 @@ class ListSubTest < Test::Unit::TestCase
345
355
  assert_equal [4, 1, 3, 2], ListMixin.where('parent_id = 5000').map(&:id)
346
356
  end
347
357
 
358
+ def test_bounds_checking
359
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:pos)
360
+
361
+ ListMixin.find(1).move_higher
362
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:pos)
363
+
364
+ ListMixin.find(4).move_lower
365
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:pos)
366
+ end
367
+
348
368
  def test_move_to_bottom_with_next_to_last_item
349
369
  assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
350
370
  ListMixin.find(3).move_to_bottom
@@ -479,6 +499,16 @@ class ArrayScopeListTest < Test::Unit::TestCase
479
499
  assert_equal [4, 1, 3, 2], ArrayScopeListMixin.where(conditions).map(&:id)
480
500
  end
481
501
 
502
+ def test_bounds_checking
503
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:pos)
504
+
505
+ ArrayScopeListMixin.find(1).move_higher
506
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:pos)
507
+
508
+ ArrayScopeListMixin.find(4).move_lower
509
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:pos)
510
+ end
511
+
482
512
  def test_move_to_bottom_with_next_to_last_item
483
513
  assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
484
514
  ArrayScopeListMixin.find(3).move_to_bottom
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sortifiable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
9
8
  - 2
10
- version: 0.1.2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Andrew White
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-02-07 00:00:00 +00:00
18
+ date: 2011-04-07 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -139,7 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
139
  requirements: []
140
140
 
141
141
  rubyforge_project:
142
- rubygems_version: 1.4.2
142
+ rubygems_version: 1.6.2
143
143
  signing_key:
144
144
  specification_version: 3
145
145
  summary: Sort your models