acts_as_list 0.1.6 → 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
@@ -4,24 +4,78 @@
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
 
10
- class TodoList < ActiveRecord::Base
11
- has_many :todo_items, :order => "position"
12
- 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
13
23
 
14
- class TodoItem < ActiveRecord::Base
15
- belongs_to :todo_list
16
- acts_as_list :scope => :todo_list
17
- 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
18
30
 
19
- todo_list.first.move_to_bottom
20
- todo_list.last.move_higher
31
+ class TodoItem < ActiveRecord::Base
32
+ belongs_to :todo_list
33
+ acts_as_list scope: :todo_list
34
+ end
21
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
+
22
72
  ## Notes
23
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.
24
74
 
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.
76
+
77
+ The `position` column is set after validations are called, so you should not put a `presence` validation on the `position` column.
78
+
25
79
  ## Versions
26
80
  All versions `0.1.5` onwards require Rails 3.0.x and higher.
27
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,10 +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]}
96
+ after_update :update_positions
97
+ before_update :check_scope
80
98
  EOV
81
99
  end
82
100
  end
@@ -135,20 +153,20 @@ module ActiveRecord
135
153
  def remove_from_list
136
154
  if in_list?
137
155
  decrement_positions_on_lower_items
138
- update_attribute position_column, nil
156
+ set_list_position(nil)
139
157
  end
140
158
  end
141
159
 
142
160
  # Increase the position of this item without adjusting the rest of the list.
143
161
  def increment_position
144
162
  return unless in_list?
145
- update_attribute position_column, self.send(position_column).to_i + 1
163
+ set_list_position(self.send(position_column).to_i + 1)
146
164
  end
147
165
 
148
166
  # Decrease the position of this item without adjusting the rest of the list.
149
167
  def decrement_position
150
168
  return unless in_list?
151
- update_attribute position_column, self.send(position_column).to_i - 1
169
+ set_list_position(self.send(position_column).to_i - 1)
152
170
  end
153
171
 
154
172
  # Return +true+ if this object is the first in the list.
@@ -166,28 +184,54 @@ module ActiveRecord
166
184
  # Return the next higher item in the list.
167
185
  def higher_item
168
186
  return nil unless in_list?
169
- acts_as_list_class.find(:first, :conditions =>
170
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
187
+ acts_as_list_class.unscoped.find(:first, :conditions =>
188
+ "#{scope_condition} AND #{position_column} < #{(send(position_column).to_i).to_s}",
189
+ :order => "#{acts_as_list_class.table_name}.#{position_column} DESC"
171
190
  )
172
191
  end
173
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
+
174
205
  # Return the next lower item in the list.
175
206
  def lower_item
176
207
  return nil unless in_list?
177
- acts_as_list_class.find(:first, :conditions =>
178
- "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
208
+ acts_as_list_class.unscoped.find(:first, :conditions =>
209
+ "#{scope_condition} AND #{position_column} > #{(send(position_column).to_i).to_s}",
210
+ :order => "#{acts_as_list_class.table_name}.#{position_column} ASC"
179
211
  )
180
212
  end
181
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
+
182
226
  # Test if this record is in a list
183
227
  def in_list?
184
228
  !not_in_list?
185
229
  end
186
-
230
+
187
231
  def not_in_list?
188
232
  send(position_column).nil?
189
233
  end
190
-
234
+
191
235
  def default_position
192
236
  acts_as_list_class.columns_hash[position_column.to_s].default
193
237
  end
@@ -196,7 +240,18 @@ module ActiveRecord
196
240
  default_position == send(position_column)
197
241
  end
198
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
+
199
249
  private
250
+ def acts_as_list_list
251
+ acts_as_list_class.unscoped.
252
+ where(scope_condition)
253
+ end
254
+
200
255
  def add_to_list_top
201
256
  increment_positions_on_all_items
202
257
  self[position_column] = acts_as_list_top
@@ -224,23 +279,25 @@ module ActiveRecord
224
279
  def bottom_item(except = nil)
225
280
  conditions = scope_condition
226
281
  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")
282
+ acts_as_list_class.unscoped.where(conditions).order("#{acts_as_list_class.table_name}.#{position_column} DESC").first
228
283
  end
229
284
 
230
285
  # Forces item to assume the bottom position in the list.
231
286
  def assume_bottom_position
232
- update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
287
+ set_list_position(bottom_position_in_list(self).to_i + 1)
233
288
  end
234
289
 
235
290
  # Forces item to assume the top position in the list.
236
291
  def assume_top_position
237
- update_attribute(position_column, acts_as_list_top)
292
+ set_list_position(acts_as_list_top)
238
293
  end
239
294
 
240
295
  # This has the effect of moving all the higher items up one.
241
296
  def decrement_positions_on_higher_items(position)
242
- acts_as_list_class.update_all(
243
- "#{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)"
244
301
  )
245
302
  end
246
303
 
@@ -248,52 +305,64 @@ module ActiveRecord
248
305
  def decrement_positions_on_lower_items(position=nil)
249
306
  return unless in_list?
250
307
  position ||= send(position_column).to_i
251
- acts_as_list_class.update_all(
252
- "#{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)"
253
312
  )
254
313
  end
255
314
 
256
315
  # This has the effect of moving all the higher items down one.
257
316
  def increment_positions_on_higher_items
258
317
  return unless in_list?
259
- acts_as_list_class.update_all(
260
- "#{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)"
261
322
  )
262
323
  end
263
324
 
264
325
  # This has the effect of moving all the lower items down one.
265
326
  def increment_positions_on_lower_items(position)
266
- acts_as_list_class.update_all(
267
- "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
268
- )
327
+ acts_as_list_class.unscoped.where(
328
+ "#{scope_condition} AND #{position_column} >= #{position}"
329
+ ).update_all(
330
+ "#{position_column} = (#{position_column} + 1)"
331
+ )
269
332
  end
270
333
 
271
334
  # Increments position (<tt>position_column</tt>) of all items in the list.
272
335
  def increment_positions_on_all_items
273
- acts_as_list_class.update_all(
274
- "#{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)"
275
340
  )
276
341
  end
277
342
 
278
343
  # 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)
344
+ def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
280
345
  return if old_position == new_position
281
-
346
+ avoid_id_condition = avoid_id ? " AND #{self.class.primary_key} != #{avoid_id}" : ''
282
347
  if old_position < new_position
283
348
  # Decrement position of intermediate items
284
349
  #
285
350
  # e.g., if moving an item from 2 to 5,
286
351
  # 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}"
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)"
289
356
  )
290
357
  else
291
358
  # Increment position of intermediate items
292
359
  #
293
360
  # e.g., if moving an item from 5 to 2,
294
361
  # 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}"
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)"
297
366
  )
298
367
  end
299
368
  end
@@ -306,18 +375,40 @@ module ActiveRecord
306
375
  else
307
376
  increment_positions_on_lower_items(position)
308
377
  end
309
- self.update_attribute(position_column, position)
378
+ set_list_position(position)
310
379
  end
311
380
 
312
381
  # used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
313
382
  def store_at_0
314
383
  if in_list?
315
384
  old_position = send(position_column).to_i
316
- update_attribute(position_column, 0)
385
+ set_list_position(0)
317
386
  decrement_positions_on_lower_items(old_position)
318
387
  end
319
388
  end
320
- end
389
+
390
+ def update_positions
391
+ old_position = send("#{position_column}_was").to_i
392
+ new_position = send(position_column).to_i
393
+ return unless acts_as_list_class.unscoped.where("#{scope_condition} AND #{position_column} = #{new_position}").count > 1
394
+ shuffle_positions_on_intermediate_items old_position, new_position, id
395
+ 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
321
412
  end
322
413
  end
323
414
  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.2.0"
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/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
@@ -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
@@ -35,8 +36,27 @@ end
35
36
 
36
37
  class Mixin < ActiveRecord::Base
37
38
  self.table_name = 'mixins'
39
+ attr_accessible :active, :parent_id, :parent_type
38
40
  end
39
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
+
40
60
  class ListMixin < Mixin
41
61
  acts_as_list :column => "pos", :scope => :parent
42
62
  end
@@ -69,6 +89,11 @@ class DefaultScopedMixin < Mixin
69
89
  default_scope { order('pos ASC') }
70
90
  end
71
91
 
92
+ class DefaultScopedWhereMixin < Mixin
93
+ acts_as_list :column => "pos"
94
+ default_scope { order('pos ASC').where(:active => true) }
95
+ end
96
+
72
97
  class TopAdditionMixin < Mixin
73
98
  acts_as_list :column => "pos", :add_new_at => :top, :scope => :parent_id
74
99
  end
@@ -158,7 +183,7 @@ end
158
183
  class DefaultScopedTest < ActsAsListTestCase
159
184
  def setup
160
185
  setup_db
161
- (1..4).each { |counter| DefaultScopedMixin.create! :pos => counter }
186
+ (1..4).each { |counter| DefaultScopedMixin.create!({:pos => counter}) }
162
187
  end
163
188
 
164
189
  def test_insert
@@ -235,18 +260,171 @@ class DefaultScopedTest < ActsAsListTestCase
235
260
  assert_equal 4, new4.pos
236
261
  end
237
262
 
263
+ def test_update_position
264
+ assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
265
+ DefaultScopedMixin.find(2).set_list_position(4)
266
+ assert_equal [1, 3, 4, 2], DefaultScopedMixin.find(:all).map(&:id)
267
+ DefaultScopedMixin.find(2).set_list_position(2)
268
+ assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
269
+ DefaultScopedMixin.find(1).set_list_position(4)
270
+ assert_equal [2, 3, 4, 1], DefaultScopedMixin.find(:all).map(&:id)
271
+ DefaultScopedMixin.find(1).set_list_position(1)
272
+ assert_equal [1, 2, 3, 4], DefaultScopedMixin.find(:all).map(&:id)
273
+ end
274
+
275
+ end
276
+
277
+ class DefaultScopedWhereTest < ActsAsListTestCase
278
+ def setup
279
+ setup_db
280
+ (1..4).each { |counter| DefaultScopedWhereMixin.create! :pos => counter, :active => false }
281
+ end
282
+
283
+ def test_insert
284
+ new = DefaultScopedWhereMixin.create
285
+ assert_equal 5, new.pos
286
+ assert !new.first?
287
+ assert new.last?
288
+
289
+ new = DefaultScopedWhereMixin.create
290
+ assert_equal 6, new.pos
291
+ assert !new.first?
292
+ assert new.last?
293
+
294
+ new = DefaultScopedWhereMixin.create
295
+ assert_equal 7, new.pos
296
+ assert !new.first?
297
+ assert new.last?
298
+ end
299
+
300
+ def test_reordering
301
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).map(&:id)
302
+
303
+ DefaultScopedWhereMixin.where(:active => false).find(2).move_lower
304
+ assert_equal [1, 3, 2, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
305
+
306
+ DefaultScopedWhereMixin.where(:active => false).find(2).move_higher
307
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
308
+
309
+ DefaultScopedWhereMixin.where(:active => false).find(1).move_to_bottom
310
+ assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
311
+
312
+ DefaultScopedWhereMixin.where(:active => false).find(1).move_to_top
313
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
314
+
315
+ DefaultScopedWhereMixin.where(:active => false).find(2).move_to_bottom
316
+ assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
317
+
318
+ DefaultScopedWhereMixin.where(:active => false).find(4).move_to_top
319
+ assert_equal [4, 1, 3, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
320
+ end
321
+
322
+ def test_insert_at
323
+ new = DefaultScopedWhereMixin.create
324
+ assert_equal 5, new.pos
325
+
326
+ new = DefaultScopedWhereMixin.create
327
+ assert_equal 6, new.pos
328
+
329
+ new = DefaultScopedWhereMixin.create
330
+ assert_equal 7, new.pos
331
+
332
+ new4 = DefaultScopedWhereMixin.create
333
+ assert_equal 8, new4.pos
334
+
335
+ new4.insert_at(2)
336
+ assert_equal 2, new4.pos
337
+
338
+ new.reload
339
+ assert_equal 8, new.pos
340
+
341
+ new.insert_at(2)
342
+ assert_equal 2, new.pos
343
+
344
+ new4.reload
345
+ assert_equal 3, new4.pos
346
+
347
+ new5 = DefaultScopedWhereMixin.create
348
+ assert_equal 9, new5.pos
349
+
350
+ new5.insert_at(1)
351
+ assert_equal 1, new5.pos
352
+
353
+ new4.reload
354
+ assert_equal 4, new4.pos
355
+ end
356
+
357
+ def test_update_position
358
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
359
+ DefaultScopedWhereMixin.where(:active => false).find(2).set_list_position(4)
360
+ assert_equal [1, 3, 4, 2], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
361
+ DefaultScopedWhereMixin.where(:active => false).find(2).set_list_position(2)
362
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
363
+ DefaultScopedWhereMixin.where(:active => false).find(1).set_list_position(4)
364
+ assert_equal [2, 3, 4, 1], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
365
+ DefaultScopedWhereMixin.where(:active => false).find(1).set_list_position(1)
366
+ assert_equal [1, 2, 3, 4], DefaultScopedWhereMixin.where(:active => false).find(:all).map(&:id)
367
+ end
368
+
369
+ end
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
238
414
  end
239
415
 
240
416
  #class TopAdditionMixin < Mixin
241
417
 
242
418
  class TopAdditionTest < ActsAsListTestCase
243
419
  include Shared::TopAddition
420
+
244
421
  def setup
245
422
  setup_db
246
423
  super
247
424
  end
248
425
  end
249
426
 
427
+
250
428
  class TopAdditionTestWithDefault < ActsAsListTestCase
251
429
  include Shared::TopAddition
252
430
 
@@ -255,3 +433,44 @@ class TopAdditionTestWithDefault < ActsAsListTestCase
255
433
  super
256
434
  end
257
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.6
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-04-19 00:00:00.000000000 Z
15
- dependencies:
16
- - !ruby/object:Gem::Dependency
17
- name: bundler
18
- requirement: &2152347900 !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: *2152347900
27
- - !ruby/object:Gem::Dependency
28
- name: activerecord
29
- requirement: &2152347320 !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: *2152347320
38
- - !ruby/object:Gem::Dependency
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
39
54
  name: rdoc
40
- requirement: &2152346840 !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: *2152346840
49
- - !ruby/object:Gem::Dependency
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
50
68
  name: sqlite3
51
- requirement: &2152346240 !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: *2152346240
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