acts_as_list 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  This `acts_as` extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a `position` column defined as an integer on the mapped database table.
6
6
 
7
+ ## Installation
8
+
9
+ In your Gemfile:
10
+
11
+ gem 'acts_as_list'
12
+
13
+ Or, from the command line:
14
+
15
+ gem install acts_as_list
7
16
 
8
17
  ## Example
9
18
 
@@ -22,6 +31,8 @@ This `acts_as` extension provides the capabilities for sorting and reordering a
22
31
  ## Notes
23
32
  If the `position` column has a default value, then there is a slight change in behavior, i.e if you have 4 items in the list, and you insert 1, with a default position 0, it would be pushed to the bottom of the list. Please look at the tests for this and some recent pull requests for discussions related to this.
24
33
 
34
+ All `position` queries (select, update, etc.) inside gem methods are executed without the default scope (i.e. `Model.unscoped`), this will prevent nasty issues when the default scope is different from `acts_as_list` scope.
35
+
25
36
  ## Versions
26
37
  All versions `0.1.5` onwards require Rails 3.0.x and higher.
27
38
 
@@ -26,8 +26,8 @@ module ActiveRecord
26
26
  # Configuration options are:
27
27
  #
28
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
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
31
  # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
32
  # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
33
  # * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
@@ -48,7 +48,7 @@ module ActiveRecord
48
48
  elsif configuration[:scope].is_a?(Array)
49
49
  scope_condition_method = %(
50
50
  def scope_condition
51
- attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
51
+ attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
52
52
  memo[column.intern] = send(column.intern); memo
53
53
  end
54
54
  self.class.send(:sanitize_sql_hash_for_conditions, attrs)
@@ -77,6 +77,7 @@ module ActiveRecord
77
77
 
78
78
  after_destroy :decrement_positions_on_lower_items
79
79
  before_create :add_to_list_#{configuration[:add_new_at]}
80
+ after_update :update_positions
80
81
  EOV
81
82
  end
82
83
  end
@@ -135,20 +136,20 @@ module ActiveRecord
135
136
  def remove_from_list
136
137
  if in_list?
137
138
  decrement_positions_on_lower_items
138
- update_attribute position_column, nil
139
+ update_attributes! position_column => nil
139
140
  end
140
141
  end
141
142
 
142
143
  # Increase the position of this item without adjusting the rest of the list.
143
144
  def increment_position
144
145
  return unless in_list?
145
- update_attribute position_column, self.send(position_column).to_i + 1
146
+ update_attributes! position_column => self.send(position_column).to_i + 1
146
147
  end
147
148
 
148
149
  # Decrease the position of this item without adjusting the rest of the list.
149
150
  def decrement_position
150
151
  return unless in_list?
151
- update_attribute position_column, self.send(position_column).to_i - 1
152
+ update_attributes! position_column => self.send(position_column).to_i - 1
152
153
  end
153
154
 
154
155
  # Return +true+ if this object is the first in the list.
@@ -166,7 +167,7 @@ module ActiveRecord
166
167
  # Return the next higher item in the list.
167
168
  def higher_item
168
169
  return nil unless in_list?
169
- acts_as_list_class.find(:first, :conditions =>
170
+ acts_as_list_class.unscoped.find(:first, :conditions =>
170
171
  "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
171
172
  )
172
173
  end
@@ -174,7 +175,7 @@ module ActiveRecord
174
175
  # Return the next lower item in the list.
175
176
  def lower_item
176
177
  return nil unless in_list?
177
- acts_as_list_class.find(:first, :conditions =>
178
+ acts_as_list_class.unscoped.find(:first, :conditions =>
178
179
  "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
179
180
  )
180
181
  end
@@ -183,11 +184,11 @@ module ActiveRecord
183
184
  def in_list?
184
185
  !not_in_list?
185
186
  end
186
-
187
+
187
188
  def not_in_list?
188
189
  send(position_column).nil?
189
190
  end
190
-
191
+
191
192
  def default_position
192
193
  acts_as_list_class.columns_hash[position_column.to_s].default
193
194
  end
@@ -224,22 +225,22 @@ module ActiveRecord
224
225
  def bottom_item(except = nil)
225
226
  conditions = scope_condition
226
227
  conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
227
- acts_as_list_class.unscoped.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
228
+ acts_as_list_class.unscoped.find(:first, :conditions => conditions, :order => "#{acts_as_list_class.table_name}.#{position_column} DESC")
228
229
  end
229
230
 
230
231
  # Forces item to assume the bottom position in the list.
231
232
  def assume_bottom_position
232
- update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
233
+ update_attributes!(position_column => bottom_position_in_list(self).to_i + 1)
233
234
  end
234
235
 
235
236
  # Forces item to assume the top position in the list.
236
237
  def assume_top_position
237
- update_attribute(position_column, acts_as_list_top)
238
+ update_attributes!(position_column => acts_as_list_top)
238
239
  end
239
240
 
240
241
  # This has the effect of moving all the higher items up one.
241
242
  def decrement_positions_on_higher_items(position)
242
- acts_as_list_class.update_all(
243
+ acts_as_list_class.unscoped.update_all(
243
244
  "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
244
245
  )
245
246
  end
@@ -248,7 +249,7 @@ module ActiveRecord
248
249
  def decrement_positions_on_lower_items(position=nil)
249
250
  return unless in_list?
250
251
  position ||= send(position_column).to_i
251
- acts_as_list_class.update_all(
252
+ acts_as_list_class.unscoped.update_all(
252
253
  "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{position}"
253
254
  )
254
255
  end
@@ -256,44 +257,44 @@ module ActiveRecord
256
257
  # This has the effect of moving all the higher items down one.
257
258
  def increment_positions_on_higher_items
258
259
  return unless in_list?
259
- acts_as_list_class.update_all(
260
+ acts_as_list_class.unscoped.update_all(
260
261
  "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
261
262
  )
262
263
  end
263
264
 
264
265
  # This has the effect of moving all the lower items down one.
265
266
  def increment_positions_on_lower_items(position)
266
- acts_as_list_class.update_all(
267
+ acts_as_list_class.unscoped.update_all(
267
268
  "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
268
269
  )
269
270
  end
270
271
 
271
272
  # Increments position (<tt>position_column</tt>) of all items in the list.
272
273
  def increment_positions_on_all_items
273
- acts_as_list_class.update_all(
274
+ acts_as_list_class.unscoped.update_all(
274
275
  "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
275
276
  )
276
277
  end
277
278
 
278
279
  # Reorders intermediate items to support moving an item from old_position to new_position.
279
- def shuffle_positions_on_intermediate_items(old_position, new_position)
280
+ def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
280
281
  return if old_position == new_position
281
-
282
+ avoid_id_condition = avoid_id ? " AND #{self.class.primary_key} != #{avoid_id}" : ''
282
283
  if old_position < new_position
283
284
  # Decrement position of intermediate items
284
285
  #
285
286
  # e.g., if moving an item from 2 to 5,
286
287
  # move [3, 4, 5] to [2, 3, 4]
287
- acts_as_list_class.update_all(
288
- "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{old_position} AND #{position_column} <= #{new_position}"
288
+ acts_as_list_class.unscoped.update_all(
289
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{old_position} AND #{position_column} <= #{new_position}#{avoid_id_condition}"
289
290
  )
290
291
  else
291
292
  # Increment position of intermediate items
292
293
  #
293
294
  # e.g., if moving an item from 5 to 2,
294
295
  # move [2, 3, 4] to [3, 4, 5]
295
- acts_as_list_class.update_all(
296
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{new_position} AND #{position_column} < #{old_position}"
296
+ acts_as_list_class.unscoped.update_all(
297
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{new_position} AND #{position_column} < #{old_position}#{avoid_id_condition}"
297
298
  )
298
299
  end
299
300
  end
@@ -306,18 +307,25 @@ module ActiveRecord
306
307
  else
307
308
  increment_positions_on_lower_items(position)
308
309
  end
309
- self.update_attribute(position_column, position)
310
+ self.update_attributes!(position_column => position)
310
311
  end
311
312
 
312
313
  # used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
313
314
  def store_at_0
314
315
  if in_list?
315
316
  old_position = send(position_column).to_i
316
- update_attribute(position_column, 0)
317
+ update_attributes!(position_column => 0)
317
318
  decrement_positions_on_lower_items(old_position)
318
319
  end
319
320
  end
320
- end
321
+
322
+ def update_positions
323
+ old_position = send("#{position_column}_was").to_i
324
+ new_position = send(position_column).to_i
325
+ return unless acts_as_list_class.unscoped.where("#{scope_condition} AND #{position_column} = #{new_position}").count > 1
326
+ shuffle_positions_on_intermediate_items old_position, new_position, id
327
+ end
328
+ end
321
329
  end
322
330
  end
323
331
  end
@@ -1,7 +1,7 @@
1
1
  module ActiveRecord
2
2
  module Acts
3
3
  module List
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.8"
5
5
  end
6
6
  end
7
7
  end
data/lib/acts_as_list.rb CHANGED
@@ -20,3 +20,5 @@ module ActsAsList
20
20
  end
21
21
  end
22
22
  end
23
+
24
+ ActsAsList::Railtie.insert
@@ -2,6 +2,7 @@ module Shared
2
2
  module ArrayScopeList
3
3
  def setup
4
4
  (1..4).each { |counter| ArrayScopeListMixin.create! :pos => counter, :parent_id => 5, :parent_type => 'ParentClass' }
5
+ (1..4).each { |counter| ArrayScopeListMixin.create! :pos => counter, :parent_id => 6, :parent_type => 'ParentClass' }
5
6
  end
6
7
 
7
8
  def test_reordering
@@ -24,6 +25,10 @@ module Shared
24
25
 
25
26
  ArrayScopeListMixin.find(4).move_to_top
26
27
  assert_equal [4, 1, 3, 2], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
28
+
29
+ ArrayScopeListMixin.find(4).insert_at(4)
30
+ assert_equal [1, 3, 2, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
31
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:pos)
27
32
  end
28
33
 
29
34
  def test_move_to_bottom_with_next_to_last_item
@@ -41,7 +46,6 @@ module Shared
41
46
 
42
47
  def test_injection
43
48
  item = ArrayScopeListMixin.new(:parent_id => 1, :parent_type => 'ParentClass')
44
- assert_equal '"mixins"."parent_id" = 1 AND "mixins"."parent_type" = \'ParentClass\'', item.scope_condition
45
49
  assert_equal "pos", item.position_column
46
50
  end
47
51
 
data/test/test_list.rb CHANGED
@@ -10,6 +10,7 @@ def setup_db(position_options = {})
10
10
  ActiveRecord::Schema.define(:version => 1) do
11
11
  create_table :mixins do |t|
12
12
  t.column :pos, :integer, position_options
13
+ t.column :active, :boolean, :default => true
13
14
  t.column :parent_id, :integer
14
15
  t.column :parent_type, :string
15
16
  t.column :created_at, :datetime
@@ -69,6 +70,11 @@ class DefaultScopedMixin < Mixin
69
70
  default_scope { order('pos ASC') }
70
71
  end
71
72
 
73
+ class DefaultScopedWhereMixin < Mixin
74
+ acts_as_list :column => "pos"
75
+ default_scope { order('pos ASC').where(:active => true) }
76
+ end
77
+
72
78
  class TopAdditionMixin < Mixin
73
79
  acts_as_list :column => "pos", :add_new_at => :top, :scope => :parent_id
74
80
  end
@@ -235,12 +241,119 @@ class DefaultScopedTest < ActsAsListTestCase
235
241
  assert_equal 4, new4.pos
236
242
  end
237
243
 
244
+ def test_update_position
245
+ assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
246
+ DefaultScopedMixin.find(2).update_attributes!(:pos => 4)
247
+ assert_equal [1, 3, 4, 2], DefaultScopedMixin.find(:all).map(&:id)
248
+ DefaultScopedMixin.find(2).update_attributes!(:pos => 2)
249
+ assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
250
+ DefaultScopedMixin.find(1).update_attributes!(:pos => 4)
251
+ assert_equal [2, 3, 4, 1], DefaultScopedMixin.find(:all).map(&:id)
252
+ DefaultScopedMixin.find(1).update_attributes!(:pos => 1)
253
+ assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
254
+ end
255
+
256
+ end
257
+
258
+ class DefaultScopedWhereTest < ActsAsListTestCase
259
+ def setup
260
+ setup_db
261
+ (1..4).each { |counter| DefaultScopedWhereMixin.create! :pos => counter, :active => false }
262
+ end
263
+
264
+ def test_insert
265
+ new = DefaultScopedWhereMixin.create
266
+ assert_equal 5, new.pos
267
+ assert !new.first?
268
+ assert new.last?
269
+
270
+ new = DefaultScopedWhereMixin.create
271
+ assert_equal 6, new.pos
272
+ assert !new.first?
273
+ assert new.last?
274
+
275
+ new = DefaultScopedWhereMixin.create
276
+ assert_equal 7, new.pos
277
+ assert !new.first?
278
+ assert new.last?
279
+ end
280
+
281
+ def test_reordering
282
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).map(&:id)
283
+
284
+ DefaultScopedWhereMixin.where(:active => false).find(2).move_lower
285
+ assert_equal [1, 3, 2, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
286
+
287
+ DefaultScopedWhereMixin.where(:active => false).find(2).move_higher
288
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
289
+
290
+ DefaultScopedWhereMixin.where(:active => false).find(1).move_to_bottom
291
+ assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
292
+
293
+ DefaultScopedWhereMixin.where(:active => false).find(1).move_to_top
294
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
295
+
296
+ DefaultScopedWhereMixin.where(:active => false).find(2).move_to_bottom
297
+ assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
298
+
299
+ DefaultScopedWhereMixin.where(:active => false).find(4).move_to_top
300
+ assert_equal [4, 1, 3, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
301
+ end
302
+
303
+ def test_insert_at
304
+ new = DefaultScopedWhereMixin.create
305
+ assert_equal 5, new.pos
306
+
307
+ new = DefaultScopedWhereMixin.create
308
+ assert_equal 6, new.pos
309
+
310
+ new = DefaultScopedWhereMixin.create
311
+ assert_equal 7, new.pos
312
+
313
+ new4 = DefaultScopedWhereMixin.create
314
+ assert_equal 8, new4.pos
315
+
316
+ new4.insert_at(2)
317
+ assert_equal 2, new4.pos
318
+
319
+ new.reload
320
+ assert_equal 8, new.pos
321
+
322
+ new.insert_at(2)
323
+ assert_equal 2, new.pos
324
+
325
+ new4.reload
326
+ assert_equal 3, new4.pos
327
+
328
+ new5 = DefaultScopedWhereMixin.create
329
+ assert_equal 9, new5.pos
330
+
331
+ new5.insert_at(1)
332
+ assert_equal 1, new5.pos
333
+
334
+ new4.reload
335
+ assert_equal 4, new4.pos
336
+ end
337
+
338
+ def test_update_position
339
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
340
+ DefaultScopedWhereMixin.where(:active => false).find(2).update_attributes!(:pos => 4)
341
+ assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
342
+ DefaultScopedWhereMixin.where(:active => false).find(2).update_attributes!(:pos => 2)
343
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
344
+ DefaultScopedWhereMixin.where(:active => false).find(1).update_attributes!(:pos => 4)
345
+ assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
346
+ DefaultScopedWhereMixin.where(:active => false).find(1).update_attributes!(:pos => 1)
347
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
348
+ end
349
+
238
350
  end
239
351
 
240
352
  #class TopAdditionMixin < Mixin
241
353
 
242
354
  class TopAdditionTest < ActsAsListTestCase
243
355
  include Shared::TopAddition
356
+
244
357
  def setup
245
358
  setup_db
246
359
  super
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_list
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,11 +11,11 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-04-19 00:00:00.000000000 Z
14
+ date: 2012-08-09 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: bundler
18
- requirement: &2152347900 !ruby/object:Gem::Requirement
18
+ requirement: &2152115680 !ruby/object:Gem::Requirement
19
19
  none: false
20
20
  requirements:
21
21
  - - ! '>='
@@ -23,10 +23,10 @@ dependencies:
23
23
  version: 1.0.0
24
24
  type: :development
25
25
  prerelease: false
26
- version_requirements: *2152347900
26
+ version_requirements: *2152115680
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
- requirement: &2152347320 !ruby/object:Gem::Requirement
29
+ requirement: &2152115040 !ruby/object:Gem::Requirement
30
30
  none: false
31
31
  requirements:
32
32
  - - ! '>='
@@ -34,10 +34,10 @@ dependencies:
34
34
  version: 1.15.4.7794
35
35
  type: :development
36
36
  prerelease: false
37
- version_requirements: *2152347320
37
+ version_requirements: *2152115040
38
38
  - !ruby/object:Gem::Dependency
39
39
  name: rdoc
40
- requirement: &2152346840 !ruby/object:Gem::Requirement
40
+ requirement: &2152114620 !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
43
  - - ! '>='
@@ -45,10 +45,10 @@ dependencies:
45
45
  version: '0'
46
46
  type: :development
47
47
  prerelease: false
48
- version_requirements: *2152346840
48
+ version_requirements: *2152114620
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: sqlite3
51
- requirement: &2152346240 !ruby/object:Gem::Requirement
51
+ requirement: &2152114020 !ruby/object:Gem::Requirement
52
52
  none: false
53
53
  requirements:
54
54
  - - ! '>='
@@ -56,7 +56,7 @@ dependencies:
56
56
  version: '0'
57
57
  type: :development
58
58
  prerelease: false
59
- version_requirements: *2152346240
59
+ version_requirements: *2152114020
60
60
  description: This "acts_as" extension provides the capabilities for sorting and reordering
61
61
  a number of objects in a list. The class that has this specified needs to have a
62
62
  "position" column defined as an integer on the mapped database table.