sortifiable 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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