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 +1 -0
- data/.travis.yml +1 -0
- data/README.md +52 -9
- data/acts_as_list.gemspec +1 -1
- data/lib/acts_as_list/active_record/acts/list.rb +116 -33
- data/lib/acts_as_list/version.rb +1 -1
- data/test/shared_array_scope_list.rb +5 -0
- data/test/shared_list.rb +11 -3
- data/test/shared_list_sub.rb +21 -1
- data/test/test_list.rb +115 -9
- metadata +88 -57
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/README.md
CHANGED
|
@@ -16,23 +16,66 @@ Or, from the command line:
|
|
|
16
16
|
|
|
17
17
|
## Example
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
244
|
-
"#{
|
|
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.
|
|
253
|
-
"#{
|
|
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.
|
|
261
|
-
"#{
|
|
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.
|
|
268
|
-
"#{
|
|
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.
|
|
275
|
-
"#{
|
|
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.
|
|
289
|
-
"#{
|
|
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.
|
|
297
|
-
"#{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/acts_as_list/version.rb
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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?
|
data/test/shared_list_sub.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module Shared
|
|
2
2
|
module ListSub
|
|
3
3
|
def setup
|
|
4
|
-
(1..4).each
|
|
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!
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
- !ruby/object:Gem::Dependency
|
|
52
|
+
version_requirements: *id002
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
39
54
|
name: rdoc
|
|
40
|
-
|
|
55
|
+
prerelease: false
|
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
|
41
57
|
none: false
|
|
42
|
-
requirements:
|
|
43
|
-
- -
|
|
44
|
-
- !ruby/object:Gem::Version
|
|
45
|
-
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
hash: 3
|
|
62
|
+
segments:
|
|
63
|
+
- 0
|
|
64
|
+
version: "0"
|
|
46
65
|
type: :development
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- !ruby/object:Gem::Dependency
|
|
66
|
+
version_requirements: *id003
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
50
68
|
name: sqlite3
|
|
51
|
-
|
|
69
|
+
prerelease: false
|
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
|
52
71
|
none: false
|
|
53
|
-
requirements:
|
|
54
|
-
- -
|
|
55
|
-
- !ruby/object:Gem::Version
|
|
56
|
-
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
hash: 3
|
|
76
|
+
segments:
|
|
77
|
+
- 0
|
|
78
|
+
version: "0"
|
|
57
79
|
type: :development
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|