acts_as_list 0.1.7 → 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/.gitignore CHANGED
@@ -4,3 +4,4 @@ Gemfile.lock
4
4
  pkg/*
5
5
  .rvmrc
6
6
  *.tmproj
7
+ .rbenv-version
data/.travis.yml CHANGED
@@ -3,3 +3,4 @@ rvm:
3
3
  - 1.8.7
4
4
  - 1.9.2
5
5
  - 1.9.3
6
+ - 2.0.0
data/README.md CHANGED
@@ -16,23 +16,66 @@ Or, from the command line:
16
16
 
17
17
  ## Example
18
18
 
19
- class TodoList < ActiveRecord::Base
20
- has_many :todo_items, :order => "position"
21
- end
19
+ At first, you need to add a `position` column to desired table:
20
+
21
+ rails g migration AddPositionToTodoItem position:integer
22
+ rake db:migrate
22
23
 
23
- class TodoItem < ActiveRecord::Base
24
- belongs_to :todo_list
25
- acts_as_list :scope => :todo_list
26
- end
24
+ After that you can use `acts_as_list` method in the model:
25
+
26
+ ```ruby
27
+ class TodoList < ActiveRecord::Base
28
+ has_many :todo_items, order: :position
29
+ end
27
30
 
28
- todo_list.first.move_to_bottom
29
- todo_list.last.move_higher
31
+ class TodoItem < ActiveRecord::Base
32
+ belongs_to :todo_list
33
+ acts_as_list scope: :todo_list
34
+ end
30
35
 
36
+ todo_list.first.move_to_bottom
37
+ todo_list.last.move_higher
38
+ ```
39
+
40
+ ## Instance Methods Added To ActiveRecord Models
41
+
42
+ You'll have a number of methods added to each instance of the ActiveRecord model that to which `acts_as_list` is added.
43
+
44
+ In `acts_as_list`, "higher" means further up the list (a lower `position`), and "lower" means further down the list (a higher `position`). That can be confusing, so it might make sense to add tests that validate that you're using the right method given your context.
45
+
46
+ ### Methods That Change Position and Reorder List
47
+
48
+ - `list_item.insert_at(2)`
49
+ - `list_item.move_lower` will do nothing if the item is the lowest item
50
+ - `list_item.move_higher` will do nothing if the item is the highest item
51
+ - `list_item.move_to_bottom`
52
+ - `list_item.move_to_top`
53
+ - `list_item.remove_from_list`
54
+
55
+ ### Methods That Change Position Without Reordering List
56
+
57
+ - `list_item.increment_position`
58
+ - `list_item.decrement_position`
59
+ - `list_item.set_list_position(3)`
60
+
61
+ ### Methods That Return Attributes of the Item's List Position
62
+ - `list_item.first?`
63
+ - `list_item.last?`
64
+ - `list_item.in_list?`
65
+ - `list_item.not_in_list?`
66
+ - `list_item.default_position?`
67
+ - `list_item.higher_item`
68
+ - `list_item.higher_items` will return all the items above `list_item` in the list (ordered by the position, ascending)
69
+ - `list_item.lower_item`
70
+ - `list_item.lower_items` will return all the items below `list_item` in the list (ordered by the position, ascending)
71
+
31
72
  ## Notes
32
73
  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.
33
74
 
34
75
  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
76
 
77
+ The `position` column is set after validations are called, so you should not put a `presence` validation on the `position` column.
78
+
36
79
  ## Versions
37
80
  All versions `0.1.5` onwards require Rails 3.0.x and higher.
38
81
 
data/acts_as_list.gemspec CHANGED
@@ -24,8 +24,8 @@ Gem::Specification.new do |s|
24
24
 
25
25
 
26
26
  # Dependencies (installed via 'bundle install')...
27
+ s.add_dependency("activerecord", [">= 3.0"])
27
28
  s.add_development_dependency("bundler", [">= 1.0.0"])
28
- s.add_development_dependency("activerecord", [">= 1.15.4.7794"])
29
29
  s.add_development_dependency("rdoc")
30
30
  s.add_development_dependency("sqlite3")
31
31
  end
@@ -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)
@@ -73,11 +73,28 @@ module ActiveRecord
73
73
  '#{configuration[:column]}'
74
74
  end
75
75
 
76
+ def scope_name
77
+ '#{configuration[:scope]}'
78
+ end
79
+
80
+ def add_new_at
81
+ '#{configuration[:add_new_at]}'
82
+ end
83
+
76
84
  #{scope_condition_method}
77
85
 
86
+ # only add to attr_accessible
87
+ # if the class has some mass_assignment_protection
88
+
89
+ if defined?(accessible_attributes) and !accessible_attributes.blank?
90
+ attr_accessible :#{configuration[:column]}
91
+ end
92
+
93
+ before_destroy :reload_position
78
94
  after_destroy :decrement_positions_on_lower_items
79
95
  before_create :add_to_list_#{configuration[:add_new_at]}
80
96
  after_update :update_positions
97
+ before_update :check_scope
81
98
  EOV
82
99
  end
83
100
  end
@@ -136,20 +153,20 @@ module ActiveRecord
136
153
  def remove_from_list
137
154
  if in_list?
138
155
  decrement_positions_on_lower_items
139
- update_attribute position_column, nil
156
+ set_list_position(nil)
140
157
  end
141
158
  end
142
159
 
143
160
  # Increase the position of this item without adjusting the rest of the list.
144
161
  def increment_position
145
162
  return unless in_list?
146
- update_attribute position_column, self.send(position_column).to_i + 1
163
+ set_list_position(self.send(position_column).to_i + 1)
147
164
  end
148
165
 
149
166
  # Decrease the position of this item without adjusting the rest of the list.
150
167
  def decrement_position
151
168
  return unless in_list?
152
- update_attribute position_column, self.send(position_column).to_i - 1
169
+ set_list_position(self.send(position_column).to_i - 1)
153
170
  end
154
171
 
155
172
  # Return +true+ if this object is the first in the list.
@@ -168,27 +185,53 @@ module ActiveRecord
168
185
  def higher_item
169
186
  return nil unless in_list?
170
187
  acts_as_list_class.unscoped.find(:first, :conditions =>
171
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
188
+ "#{scope_condition} AND #{position_column} < #{(send(position_column).to_i).to_s}",
189
+ :order => "#{acts_as_list_class.table_name}.#{position_column} DESC"
172
190
  )
173
191
  end
174
192
 
193
+ # Return the next n higher items in the list
194
+ # selects all higher items by default
195
+ def higher_items(limit=nil)
196
+ limit ||= acts_as_list_list.count
197
+ position_value = send(position_column)
198
+ acts_as_list_list.
199
+ where("#{position_column} < ?", position_value).
200
+ where("#{position_column} >= ?", position_value - limit).
201
+ limit(limit).
202
+ order("#{acts_as_list_class.table_name}.#{position_column} ASC")
203
+ end
204
+
175
205
  # Return the next lower item in the list.
176
206
  def lower_item
177
207
  return nil unless in_list?
178
208
  acts_as_list_class.unscoped.find(:first, :conditions =>
179
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
209
+ "#{scope_condition} AND #{position_column} > #{(send(position_column).to_i).to_s}",
210
+ :order => "#{acts_as_list_class.table_name}.#{position_column} ASC"
180
211
  )
181
212
  end
182
213
 
214
+ # Return the next n lower items in the list
215
+ # selects all lower items by default
216
+ def lower_items(limit=nil)
217
+ limit ||= acts_as_list_list.count
218
+ position_value = send(position_column)
219
+ acts_as_list_list.
220
+ where("#{position_column} > ?", position_value).
221
+ where("#{position_column} <= ?", position_value + limit).
222
+ limit(limit).
223
+ order("#{acts_as_list_class.table_name}.#{position_column} ASC")
224
+ end
225
+
183
226
  # Test if this record is in a list
184
227
  def in_list?
185
228
  !not_in_list?
186
229
  end
187
-
230
+
188
231
  def not_in_list?
189
232
  send(position_column).nil?
190
233
  end
191
-
234
+
192
235
  def default_position
193
236
  acts_as_list_class.columns_hash[position_column.to_s].default
194
237
  end
@@ -197,7 +240,18 @@ module ActiveRecord
197
240
  default_position == send(position_column)
198
241
  end
199
242
 
243
+ # Sets the new position and saves it
244
+ def set_list_position(new_position)
245
+ send("#{position_column}=", new_position)
246
+ save!
247
+ end
248
+
200
249
  private
250
+ def acts_as_list_list
251
+ acts_as_list_class.unscoped.
252
+ where(scope_condition)
253
+ end
254
+
201
255
  def add_to_list_top
202
256
  increment_positions_on_all_items
203
257
  self[position_column] = acts_as_list_top
@@ -225,23 +279,25 @@ module ActiveRecord
225
279
  def bottom_item(except = nil)
226
280
  conditions = scope_condition
227
281
  conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
228
- acts_as_list_class.unscoped.find(:first, :conditions => conditions, :order => "#{acts_as_list_class.table_name}.#{position_column} DESC")
282
+ acts_as_list_class.unscoped.where(conditions).order("#{acts_as_list_class.table_name}.#{position_column} DESC").first
229
283
  end
230
284
 
231
285
  # Forces item to assume the bottom position in the list.
232
286
  def assume_bottom_position
233
- update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
287
+ set_list_position(bottom_position_in_list(self).to_i + 1)
234
288
  end
235
289
 
236
290
  # Forces item to assume the top position in the list.
237
291
  def assume_top_position
238
- update_attribute(position_column, acts_as_list_top)
292
+ set_list_position(acts_as_list_top)
239
293
  end
240
294
 
241
295
  # This has the effect of moving all the higher items up one.
242
296
  def decrement_positions_on_higher_items(position)
243
- acts_as_list_class.unscoped.update_all(
244
- "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
297
+ acts_as_list_class.unscoped.where(
298
+ "#{scope_condition} AND #{position_column} <= #{position}"
299
+ ).update_all(
300
+ "#{position_column} = (#{position_column} - 1)"
245
301
  )
246
302
  end
247
303
 
@@ -249,30 +305,38 @@ module ActiveRecord
249
305
  def decrement_positions_on_lower_items(position=nil)
250
306
  return unless in_list?
251
307
  position ||= send(position_column).to_i
252
- acts_as_list_class.unscoped.update_all(
253
- "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{position}"
308
+ acts_as_list_class.unscoped.where(
309
+ "#{scope_condition} AND #{position_column} > #{position}"
310
+ ).update_all(
311
+ "#{position_column} = (#{position_column} - 1)"
254
312
  )
255
313
  end
256
314
 
257
315
  # This has the effect of moving all the higher items down one.
258
316
  def increment_positions_on_higher_items
259
317
  return unless in_list?
260
- acts_as_list_class.unscoped.update_all(
261
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
318
+ acts_as_list_class.unscoped.where(
319
+ "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
320
+ ).update_all(
321
+ "#{position_column} = (#{position_column} + 1)"
262
322
  )
263
323
  end
264
324
 
265
325
  # This has the effect of moving all the lower items down one.
266
326
  def increment_positions_on_lower_items(position)
267
- acts_as_list_class.unscoped.update_all(
268
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
269
- )
327
+ acts_as_list_class.unscoped.where(
328
+ "#{scope_condition} AND #{position_column} >= #{position}"
329
+ ).update_all(
330
+ "#{position_column} = (#{position_column} + 1)"
331
+ )
270
332
  end
271
333
 
272
334
  # Increments position (<tt>position_column</tt>) of all items in the list.
273
335
  def increment_positions_on_all_items
274
- acts_as_list_class.unscoped.update_all(
275
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
336
+ acts_as_list_class.unscoped.where(
337
+ "#{scope_condition}"
338
+ ).update_all(
339
+ "#{position_column} = (#{position_column} + 1)"
276
340
  )
277
341
  end
278
342
 
@@ -285,16 +349,20 @@ module ActiveRecord
285
349
  #
286
350
  # e.g., if moving an item from 2 to 5,
287
351
  # move [3, 4, 5] to [2, 3, 4]
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}"
352
+ acts_as_list_class.unscoped.where(
353
+ "#{scope_condition} AND #{position_column} > #{old_position} AND #{position_column} <= #{new_position}#{avoid_id_condition}"
354
+ ).update_all(
355
+ "#{position_column} = (#{position_column} - 1)"
290
356
  )
291
357
  else
292
358
  # Increment position of intermediate items
293
359
  #
294
360
  # e.g., if moving an item from 5 to 2,
295
361
  # move [2, 3, 4] to [3, 4, 5]
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}"
362
+ acts_as_list_class.unscoped.where(
363
+ "#{scope_condition} AND #{position_column} >= #{new_position} AND #{position_column} < #{old_position}#{avoid_id_condition}"
364
+ ).update_all(
365
+ "#{position_column} = (#{position_column} + 1)"
298
366
  )
299
367
  end
300
368
  end
@@ -307,25 +375,40 @@ module ActiveRecord
307
375
  else
308
376
  increment_positions_on_lower_items(position)
309
377
  end
310
- self.update_attribute(position_column, position)
378
+ set_list_position(position)
311
379
  end
312
380
 
313
381
  # used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
314
382
  def store_at_0
315
383
  if in_list?
316
384
  old_position = send(position_column).to_i
317
- update_attribute(position_column, 0)
385
+ set_list_position(0)
318
386
  decrement_positions_on_lower_items(old_position)
319
387
  end
320
388
  end
321
-
389
+
322
390
  def update_positions
323
391
  old_position = send("#{position_column}_was").to_i
324
392
  new_position = send(position_column).to_i
325
- return unless acts_as_list_class.unscoped.where("#{position_column} = #{new_position}").count > 1
393
+ return unless acts_as_list_class.unscoped.where("#{scope_condition} AND #{position_column} = #{new_position}").count > 1
326
394
  shuffle_positions_on_intermediate_items old_position, new_position, id
327
395
  end
328
- end
396
+
397
+ def check_scope
398
+ if changes.include?("#{scope_name}")
399
+ old_scope_id = changes["#{scope_name}"].first
400
+ new_scope_id = changes["#{scope_name}"].last
401
+ self["#{scope_name}"] = old_scope_id
402
+ send("decrement_positions_on_lower_items")
403
+ self["#{scope_name}"] = new_scope_id
404
+ send("add_to_list_#{add_new_at}")
405
+ end
406
+ end
407
+
408
+ def reload_position
409
+ self.reload
410
+ end
411
+ end
329
412
  end
330
413
  end
331
414
  end
@@ -1,7 +1,7 @@
1
1
  module ActiveRecord
2
2
  module Acts
3
3
  module List
4
- VERSION = "0.1.7"
4
+ VERSION = "0.2.0"
5
5
  end
6
6
  end
7
7
  end
@@ -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
data/test/shared_list.rb CHANGED
@@ -1,7 +1,11 @@
1
1
  module Shared
2
2
  module List
3
3
  def setup
4
- (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
4
+ (1..4).each do |counter|
5
+ node = ListMixin.new :parent_id => 5
6
+ node.pos = counter
7
+ node.save!
8
+ end
5
9
  end
6
10
 
7
11
  def test_reordering
@@ -204,14 +208,18 @@ module Shared
204
208
  def test_before_create_callback_adds_to_given_position
205
209
  assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
206
210
 
207
- new = ListMixin.create(:pos => 1, :parent_id => 5)
211
+ new = ListMixin.new(:parent_id => 5)
212
+ new.pos = 1
213
+ new.save!
208
214
  assert_equal 1, new.pos
209
215
  assert new.first?
210
216
  assert !new.last?
211
217
 
212
218
  assert_equal [5, 1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
213
219
 
214
- new = ListMixin.create(:pos => 3, :parent_id => 5)
220
+ new = ListMixin.new(:parent_id => 5)
221
+ new.pos = 3
222
+ new.save!
215
223
  assert_equal 3, new.pos
216
224
  assert !new.first?
217
225
  assert !new.last?
@@ -1,7 +1,11 @@
1
1
  module Shared
2
2
  module ListSub
3
3
  def setup
4
- (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
4
+ (1..4).each do |i|
5
+ node = ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).new :parent_id => 5000
6
+ node.pos = i
7
+ node.save!
8
+ end
5
9
  end
6
10
 
7
11
  def test_reordering
@@ -39,6 +43,22 @@ module Shared
39
43
  assert_nil ListMixin.find(4).lower_item
40
44
  end
41
45
 
46
+ def test_next_prev_groups
47
+ li1 = ListMixin.find(1)
48
+ li2 = ListMixin.find(2)
49
+ li3 = ListMixin.find(3)
50
+ li4 = ListMixin.find(4)
51
+ assert_equal [li2, li3, li4], li1.lower_items
52
+ assert_equal [li4], li3.lower_items
53
+ assert_equal [li2, li3], li1.lower_items(2)
54
+ assert_equal [], li4.lower_items
55
+
56
+ assert_equal [li1, li2], li3.higher_items
57
+ assert_equal [li1], li2.higher_items
58
+ assert_equal [li2, li3], li4.higher_items(2)
59
+ assert_equal [], li1.higher_items
60
+ end
61
+
42
62
  def test_injection
43
63
  item = ListMixin.new("parent_id"=>1)
44
64
  assert_equal '"mixins"."parent_id" = 1', item.scope_condition
data/test/test_list.rb CHANGED
@@ -36,8 +36,27 @@ end
36
36
 
37
37
  class Mixin < ActiveRecord::Base
38
38
  self.table_name = 'mixins'
39
+ attr_accessible :active, :parent_id, :parent_type
39
40
  end
40
41
 
42
+ class ProtectedMixin < ActiveRecord::Base
43
+ self.table_name = 'mixins'
44
+ attr_protected :active
45
+ end
46
+
47
+ class ProtectedListMixin < ProtectedMixin
48
+ acts_as_list :column => "pos"
49
+ end
50
+
51
+ class UnProtectedMixin < ActiveRecord::Base
52
+ self.table_name = 'mixins'
53
+ end
54
+
55
+ class UnProtectedListMixin < UnProtectedMixin
56
+ acts_as_list :column => "pos"
57
+ end
58
+
59
+
41
60
  class ListMixin < Mixin
42
61
  acts_as_list :column => "pos", :scope => :parent
43
62
  end
@@ -164,7 +183,7 @@ end
164
183
  class DefaultScopedTest < ActsAsListTestCase
165
184
  def setup
166
185
  setup_db
167
- (1..4).each { |counter| DefaultScopedMixin.create! :pos => counter }
186
+ (1..4).each { |counter| DefaultScopedMixin.create!({:pos => counter}) }
168
187
  end
169
188
 
170
189
  def test_insert
@@ -243,13 +262,13 @@ class DefaultScopedTest < ActsAsListTestCase
243
262
 
244
263
  def test_update_position
245
264
  assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
246
- DefaultScopedMixin.find(2).update_attribute(:pos, 4)
265
+ DefaultScopedMixin.find(2).set_list_position(4)
247
266
  assert_equal [1, 3, 4, 2], DefaultScopedMixin.find(:all).map(&:id)
248
- DefaultScopedMixin.find(2).update_attribute(:pos, 2)
267
+ DefaultScopedMixin.find(2).set_list_position(2)
249
268
  assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
250
- DefaultScopedMixin.find(1).update_attribute(:pos, 4)
269
+ DefaultScopedMixin.find(1).set_list_position(4)
251
270
  assert_equal [2, 3, 4, 1], DefaultScopedMixin.find(:all).map(&:id)
252
- DefaultScopedMixin.find(1).update_attribute(:pos, 1)
271
+ DefaultScopedMixin.find(1).set_list_position(1)
253
272
  assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
254
273
  end
255
274
 
@@ -337,18 +356,63 @@ class DefaultScopedWhereTest < ActsAsListTestCase
337
356
 
338
357
  def test_update_position
339
358
  assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
340
- DefaultScopedWhereMixin.where(:active => false).find(2).update_attribute(:pos, 4)
359
+ DefaultScopedWhereMixin.where(:active => false).find(2).set_list_position(4)
341
360
  assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
342
- DefaultScopedWhereMixin.where(:active => false).find(2).update_attribute(:pos, 2)
361
+ DefaultScopedWhereMixin.where(:active => false).find(2).set_list_position(2)
343
362
  assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
344
- DefaultScopedWhereMixin.where(:active => false).find(1).update_attribute(:pos, 4)
363
+ DefaultScopedWhereMixin.where(:active => false).find(1).set_list_position(4)
345
364
  assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
346
- DefaultScopedWhereMixin.where(:active => false).find(1).update_attribute(:pos, 1)
365
+ DefaultScopedWhereMixin.where(:active => false).find(1).set_list_position(1)
347
366
  assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
348
367
  end
349
368
 
350
369
  end
351
370
 
371
+ class MultiDestroyTest < ActsAsListTestCase
372
+
373
+ def setup
374
+ setup_db
375
+ end
376
+
377
+ # example:
378
+ #
379
+ # class TodoList < ActiveRecord::Base
380
+ # has_many :todo_items, :order => "position"
381
+ # accepts_nested_attributes_for :todo_items, :allow_destroy => true
382
+ # end
383
+ #
384
+ # class TodoItem < ActiveRecord::Base
385
+ # belongs_to :todo_list
386
+ # acts_as_list :scope => :todo_list
387
+ # end
388
+ #
389
+ # Assume that there are three items.
390
+ # The user mark two items as deleted, click save button, form will be post:
391
+ #
392
+ # todo_list.todo_items_attributes = [
393
+ # {id: 1, _destroy: true},
394
+ # {id: 2, _destroy: true}
395
+ # ]
396
+ #
397
+ # Save toto_list, the position of item #3 should eql 1.
398
+ #
399
+ def test_destroy
400
+ new1 = DefaultScopedMixin.create
401
+ assert_equal 1, new1.pos
402
+
403
+ new2 = DefaultScopedMixin.create
404
+ assert_equal 2, new2.pos
405
+
406
+ new3 = DefaultScopedMixin.create
407
+ assert_equal 3, new3.pos
408
+
409
+ new1.destroy
410
+ new2.destroy
411
+ new3.reload
412
+ assert_equal 1, new3.pos
413
+ end
414
+ end
415
+
352
416
  #class TopAdditionMixin < Mixin
353
417
 
354
418
  class TopAdditionTest < ActsAsListTestCase
@@ -360,6 +424,7 @@ class TopAdditionTest < ActsAsListTestCase
360
424
  end
361
425
  end
362
426
 
427
+
363
428
  class TopAdditionTestWithDefault < ActsAsListTestCase
364
429
  include Shared::TopAddition
365
430
 
@@ -368,3 +433,44 @@ class TopAdditionTestWithDefault < ActsAsListTestCase
368
433
  super
369
434
  end
370
435
  end
436
+
437
+ class RespectMixinProtection < ActsAsListTestCase
438
+ def setup
439
+ setup_db_with_default
440
+ super
441
+ end
442
+
443
+ # if an attribute is set attr_protected
444
+ # it should be unchanged by update_attributes
445
+ def test_unmodified_protection
446
+ a = ProtectedMixin.new
447
+ a.update_attributes({:active => false})
448
+ assert_equal true, a.active
449
+ end
450
+
451
+ # even after the acts_as_list mixin is joined
452
+ # that protection should continue to exist
453
+ def test_still_protected
454
+ b = ProtectedListMixin.new
455
+ b.update_attributes({:active => false})
456
+ assert_equal true, b.active
457
+ end
458
+
459
+ # similarly, if a class lacks mass_assignment protection
460
+ # it should be able to be changed
461
+ def test_unprotected
462
+ a = UnProtectedMixin.new
463
+ a.update_attributes({:active => false})
464
+ assert_equal false, a.active
465
+ end
466
+
467
+ # and it should continue to be mutable by mass_assignment
468
+ # even after the acts_as_list plugin has been joined
469
+ def test_still_unprotected_mixin
470
+ b = UnProtectedListMixin.new
471
+ b.assign_attributes({:active => false})
472
+ # p UnProtectedListMixin.accessible_attributes.length
473
+ assert_equal false, b.active
474
+ end
475
+
476
+ end
metadata CHANGED
@@ -1,71 +1,93 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: acts_as_list
3
- version: !ruby/object:Gem::Version
4
- version: 0.1.7
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - David Heinemeier Hansson
9
14
  - Swanand Pagnis
10
15
  - Quinn Chaffee
11
16
  autorequire:
12
17
  bindir: bin
13
18
  cert_chain: []
14
- date: 2012-07-25 00:00:00.000000000 Z
15
- dependencies:
16
- - !ruby/object:Gem::Dependency
17
- name: bundler
18
- requirement: &2156341060 !ruby/object:Gem::Requirement
19
+
20
+ date: 2013-02-28 00:00:00 Z
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: activerecord
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
19
26
  none: false
20
- requirements:
21
- - - ! '>='
22
- - !ruby/object:Gem::Version
23
- version: 1.0.0
24
- type: :development
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 3
33
+ - 0
34
+ version: "3.0"
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: bundler
25
39
  prerelease: false
26
- version_requirements: *2156341060
27
- - !ruby/object:Gem::Dependency
28
- name: activerecord
29
- requirement: &2156340520 !ruby/object:Gem::Requirement
40
+ requirement: &id002 !ruby/object:Gem::Requirement
30
41
  none: false
31
- requirements:
32
- - - ! '>='
33
- - !ruby/object:Gem::Version
34
- version: 1.15.4.7794
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 23
46
+ segments:
47
+ - 1
48
+ - 0
49
+ - 0
50
+ version: 1.0.0
35
51
  type: :development
36
- prerelease: false
37
- version_requirements: *2156340520
38
- - !ruby/object:Gem::Dependency
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
39
54
  name: rdoc
40
- requirement: &2156340020 !ruby/object:Gem::Requirement
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
41
57
  none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: '0'
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
46
65
  type: :development
47
- prerelease: false
48
- version_requirements: *2156340020
49
- - !ruby/object:Gem::Dependency
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
50
68
  name: sqlite3
51
- requirement: &2156339420 !ruby/object:Gem::Requirement
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
52
71
  none: false
53
- requirements:
54
- - - ! '>='
55
- - !ruby/object:Gem::Version
56
- version: '0'
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
57
79
  type: :development
58
- prerelease: false
59
- version_requirements: *2156339420
60
- description: This "acts_as" extension provides the capabilities for sorting and reordering
61
- a number of objects in a list. The class that has this specified needs to have a
62
- "position" column defined as an integer on the mapped database table.
63
- email:
80
+ version_requirements: *id004
81
+ description: 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.
82
+ email:
64
83
  - swanand.pagnis@gmail.com
65
84
  executables: []
85
+
66
86
  extensions: []
87
+
67
88
  extra_rdoc_files: []
68
- files:
89
+
90
+ files:
69
91
  - .gemtest
70
92
  - .gitignore
71
93
  - .travis.yml
@@ -87,29 +109,38 @@ files:
87
109
  - test/test_list.rb
88
110
  homepage: http://github.com/swanandp/acts_as_list
89
111
  licenses: []
112
+
90
113
  post_install_message:
91
114
  rdoc_options: []
92
- require_paths:
115
+
116
+ require_paths:
93
117
  - lib
94
- required_ruby_version: !ruby/object:Gem::Requirement
118
+ required_ruby_version: !ruby/object:Gem::Requirement
95
119
  none: false
96
- requirements:
97
- - - ! '>='
98
- - !ruby/object:Gem::Version
99
- version: '0'
100
- required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ hash: 3
124
+ segments:
125
+ - 0
126
+ version: "0"
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
128
  none: false
102
- requirements:
103
- - - ! '>='
104
- - !ruby/object:Gem::Version
105
- version: '0'
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ hash: 3
133
+ segments:
134
+ - 0
135
+ version: "0"
106
136
  requirements: []
137
+
107
138
  rubyforge_project: acts_as_list
108
139
  rubygems_version: 1.8.15
109
140
  signing_key:
110
141
  specification_version: 3
111
142
  summary: A gem allowing a active_record model to act_as_list.
112
- test_files:
143
+ test_files:
113
144
  - test/helper.rb
114
145
  - test/shared.rb
115
146
  - test/shared_array_scope_list.rb