acts_as_list 0.1.5 → 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 +6 -0
- data/Gemfile +2 -0
- data/README.md +76 -10
- data/Rakefile +1 -3
- data/acts_as_list.gemspec +2 -2
- data/init.rb +2 -0
- data/lib/acts_as_list/active_record/acts/list.rb +131 -38
- data/lib/acts_as_list/version.rb +1 -1
- data/lib/acts_as_list.rb +23 -1
- data/test/shared.rb +1 -0
- data/test/shared_array_scope_list.rb +5 -1
- data/test/shared_list.rb +11 -14
- data/test/shared_list_sub.rb +22 -2
- data/test/shared_top_addition.rb +87 -0
- data/test/test_list.rb +243 -1
- metadata +91 -58
- data/README.rdoc +0 -35
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -2,26 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
## Description
|
|
4
4
|
|
|
5
|
-
This `acts_as extension
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
+
|
|
79
|
+
## Versions
|
|
80
|
+
All versions `0.1.5` onwards require Rails 3.0.x and higher.
|
|
81
|
+
|
|
82
|
+
## Build Status
|
|
83
|
+
[](https://secure.travis-ci.org/swanandp/acts_as_list)
|
|
84
|
+
|
|
85
|
+
## Roadmap
|
|
86
|
+
|
|
87
|
+
1. Sort based feature
|
|
88
|
+
2. Rails 4 compatibility and bye bye Rails 2! Older versions would of course continue to work with Rails 2, but there won't be any support on those.
|
|
89
|
+
|
|
25
90
|
## Contributing to `acts_as_list`
|
|
26
91
|
|
|
27
92
|
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
|
@@ -31,6 +96,7 @@ If the `position` column has a default value, then there is a slight change in b
|
|
|
31
96
|
- Commit and push until you are happy with your contribution
|
|
32
97
|
- Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
|
33
98
|
- Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
|
99
|
+
- I would recommend using Rails 3.1.x and higher for testing the build before a pull request. The current test harness does not quite work with 3.0.x. The plugin itself works, but the issue lies with testing infrastructure.
|
|
34
100
|
|
|
35
101
|
## Copyright
|
|
36
102
|
|
data/Rakefile
CHANGED
|
@@ -12,11 +12,9 @@ desc 'Test the acts_as_list plugin.'
|
|
|
12
12
|
Rake::TestTask.new(:test) do |t|
|
|
13
13
|
t.libs << 'lib' << 'test'
|
|
14
14
|
t.pattern = 'test/**/test_*.rb'
|
|
15
|
-
t.verbose =
|
|
15
|
+
t.verbose = false
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
18
|
# Run the rdoc task to generate rdocs for this gem
|
|
21
19
|
require 'rdoc/task'
|
|
22
20
|
RDoc::Task.new do |rdoc|
|
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.
|
|
28
|
-
s.add_development_dependency("
|
|
27
|
+
s.add_dependency("activerecord", [">= 3.0"])
|
|
28
|
+
s.add_development_dependency("bundler", [">= 1.0.0"])
|
|
29
29
|
s.add_development_dependency("rdoc")
|
|
30
30
|
s.add_development_dependency("sqlite3")
|
|
31
31
|
end
|
data/init.rb
CHANGED
|
@@ -26,14 +26,15 @@ 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
|
|
34
34
|
# act more like an array in its indexing.
|
|
35
|
+
# * +add_new_at+ - specifies whether objects get added to the :top or :bottom of the list. (default: +bottom+)
|
|
35
36
|
def acts_as_list(options = {})
|
|
36
|
-
configuration = { :column => "position", :scope => "1 = 1", :top_of_list => 1}
|
|
37
|
+
configuration = { :column => "position", :scope => "1 = 1", :top_of_list => 1, :add_new_at => :bottom}
|
|
37
38
|
configuration.update(options) if options.is_a?(Hash)
|
|
38
39
|
|
|
39
40
|
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
|
@@ -47,7 +48,7 @@ module ActiveRecord
|
|
|
47
48
|
elsif configuration[:scope].is_a?(Array)
|
|
48
49
|
scope_condition_method = %(
|
|
49
50
|
def scope_condition
|
|
50
|
-
attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
|
|
51
|
+
attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
|
|
51
52
|
memo[column.intern] = send(column.intern); memo
|
|
52
53
|
end
|
|
53
54
|
self.class.send(:sanitize_sql_hash_for_conditions, attrs)
|
|
@@ -58,7 +59,7 @@ module ActiveRecord
|
|
|
58
59
|
end
|
|
59
60
|
|
|
60
61
|
class_eval <<-EOV
|
|
61
|
-
include ActiveRecord::Acts::List::InstanceMethods
|
|
62
|
+
include ::ActiveRecord::Acts::List::InstanceMethods
|
|
62
63
|
|
|
63
64
|
def acts_as_list_top
|
|
64
65
|
#{configuration[:top_of_list]}.to_i
|
|
@@ -72,10 +73,28 @@ module ActiveRecord
|
|
|
72
73
|
'#{configuration[:column]}'
|
|
73
74
|
end
|
|
74
75
|
|
|
76
|
+
def scope_name
|
|
77
|
+
'#{configuration[:scope]}'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_new_at
|
|
81
|
+
'#{configuration[:add_new_at]}'
|
|
82
|
+
end
|
|
83
|
+
|
|
75
84
|
#{scope_condition_method}
|
|
76
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
|
|
77
94
|
after_destroy :decrement_positions_on_lower_items
|
|
78
|
-
|
|
95
|
+
before_create :add_to_list_#{configuration[:add_new_at]}
|
|
96
|
+
after_update :update_positions
|
|
97
|
+
before_update :check_scope
|
|
79
98
|
EOV
|
|
80
99
|
end
|
|
81
100
|
end
|
|
@@ -134,20 +153,20 @@ module ActiveRecord
|
|
|
134
153
|
def remove_from_list
|
|
135
154
|
if in_list?
|
|
136
155
|
decrement_positions_on_lower_items
|
|
137
|
-
|
|
156
|
+
set_list_position(nil)
|
|
138
157
|
end
|
|
139
158
|
end
|
|
140
159
|
|
|
141
160
|
# Increase the position of this item without adjusting the rest of the list.
|
|
142
161
|
def increment_position
|
|
143
162
|
return unless in_list?
|
|
144
|
-
|
|
163
|
+
set_list_position(self.send(position_column).to_i + 1)
|
|
145
164
|
end
|
|
146
165
|
|
|
147
166
|
# Decrease the position of this item without adjusting the rest of the list.
|
|
148
167
|
def decrement_position
|
|
149
168
|
return unless in_list?
|
|
150
|
-
|
|
169
|
+
set_list_position(self.send(position_column).to_i - 1)
|
|
151
170
|
end
|
|
152
171
|
|
|
153
172
|
# Return +true+ if this object is the first in the list.
|
|
@@ -165,28 +184,54 @@ module ActiveRecord
|
|
|
165
184
|
# Return the next higher item in the list.
|
|
166
185
|
def higher_item
|
|
167
186
|
return nil unless in_list?
|
|
168
|
-
acts_as_list_class.find(:first, :conditions =>
|
|
169
|
-
"#{scope_condition} AND #{position_column}
|
|
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"
|
|
170
190
|
)
|
|
171
191
|
end
|
|
172
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
|
+
|
|
173
205
|
# Return the next lower item in the list.
|
|
174
206
|
def lower_item
|
|
175
207
|
return nil unless in_list?
|
|
176
|
-
acts_as_list_class.find(:first, :conditions =>
|
|
177
|
-
"#{scope_condition} AND #{position_column}
|
|
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"
|
|
178
211
|
)
|
|
179
212
|
end
|
|
180
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
|
+
|
|
181
226
|
# Test if this record is in a list
|
|
182
227
|
def in_list?
|
|
183
228
|
!not_in_list?
|
|
184
229
|
end
|
|
185
|
-
|
|
230
|
+
|
|
186
231
|
def not_in_list?
|
|
187
232
|
send(position_column).nil?
|
|
188
233
|
end
|
|
189
|
-
|
|
234
|
+
|
|
190
235
|
def default_position
|
|
191
236
|
acts_as_list_class.columns_hash[position_column.to_s].default
|
|
192
237
|
end
|
|
@@ -195,9 +240,21 @@ module ActiveRecord
|
|
|
195
240
|
default_position == send(position_column)
|
|
196
241
|
end
|
|
197
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
|
+
|
|
198
249
|
private
|
|
250
|
+
def acts_as_list_list
|
|
251
|
+
acts_as_list_class.unscoped.
|
|
252
|
+
where(scope_condition)
|
|
253
|
+
end
|
|
254
|
+
|
|
199
255
|
def add_to_list_top
|
|
200
256
|
increment_positions_on_all_items
|
|
257
|
+
self[position_column] = acts_as_list_top
|
|
201
258
|
end
|
|
202
259
|
|
|
203
260
|
def add_to_list_bottom
|
|
@@ -222,23 +279,25 @@ module ActiveRecord
|
|
|
222
279
|
def bottom_item(except = nil)
|
|
223
280
|
conditions = scope_condition
|
|
224
281
|
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
|
|
225
|
-
acts_as_list_class.unscoped.
|
|
282
|
+
acts_as_list_class.unscoped.where(conditions).order("#{acts_as_list_class.table_name}.#{position_column} DESC").first
|
|
226
283
|
end
|
|
227
284
|
|
|
228
285
|
# Forces item to assume the bottom position in the list.
|
|
229
286
|
def assume_bottom_position
|
|
230
|
-
|
|
287
|
+
set_list_position(bottom_position_in_list(self).to_i + 1)
|
|
231
288
|
end
|
|
232
289
|
|
|
233
290
|
# Forces item to assume the top position in the list.
|
|
234
291
|
def assume_top_position
|
|
235
|
-
|
|
292
|
+
set_list_position(acts_as_list_top)
|
|
236
293
|
end
|
|
237
294
|
|
|
238
295
|
# This has the effect of moving all the higher items up one.
|
|
239
296
|
def decrement_positions_on_higher_items(position)
|
|
240
|
-
acts_as_list_class.
|
|
241
|
-
"#{
|
|
297
|
+
acts_as_list_class.unscoped.where(
|
|
298
|
+
"#{scope_condition} AND #{position_column} <= #{position}"
|
|
299
|
+
).update_all(
|
|
300
|
+
"#{position_column} = (#{position_column} - 1)"
|
|
242
301
|
)
|
|
243
302
|
end
|
|
244
303
|
|
|
@@ -246,52 +305,64 @@ module ActiveRecord
|
|
|
246
305
|
def decrement_positions_on_lower_items(position=nil)
|
|
247
306
|
return unless in_list?
|
|
248
307
|
position ||= send(position_column).to_i
|
|
249
|
-
acts_as_list_class.
|
|
250
|
-
"#{
|
|
308
|
+
acts_as_list_class.unscoped.where(
|
|
309
|
+
"#{scope_condition} AND #{position_column} > #{position}"
|
|
310
|
+
).update_all(
|
|
311
|
+
"#{position_column} = (#{position_column} - 1)"
|
|
251
312
|
)
|
|
252
313
|
end
|
|
253
314
|
|
|
254
315
|
# This has the effect of moving all the higher items down one.
|
|
255
316
|
def increment_positions_on_higher_items
|
|
256
317
|
return unless in_list?
|
|
257
|
-
acts_as_list_class.
|
|
258
|
-
"#{
|
|
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)"
|
|
259
322
|
)
|
|
260
323
|
end
|
|
261
324
|
|
|
262
325
|
# This has the effect of moving all the lower items down one.
|
|
263
326
|
def increment_positions_on_lower_items(position)
|
|
264
|
-
acts_as_list_class.
|
|
265
|
-
"#{
|
|
266
|
-
|
|
327
|
+
acts_as_list_class.unscoped.where(
|
|
328
|
+
"#{scope_condition} AND #{position_column} >= #{position}"
|
|
329
|
+
).update_all(
|
|
330
|
+
"#{position_column} = (#{position_column} + 1)"
|
|
331
|
+
)
|
|
267
332
|
end
|
|
268
333
|
|
|
269
334
|
# Increments position (<tt>position_column</tt>) of all items in the list.
|
|
270
335
|
def increment_positions_on_all_items
|
|
271
|
-
acts_as_list_class.
|
|
272
|
-
"#{
|
|
336
|
+
acts_as_list_class.unscoped.where(
|
|
337
|
+
"#{scope_condition}"
|
|
338
|
+
).update_all(
|
|
339
|
+
"#{position_column} = (#{position_column} + 1)"
|
|
273
340
|
)
|
|
274
341
|
end
|
|
275
342
|
|
|
276
343
|
# Reorders intermediate items to support moving an item from old_position to new_position.
|
|
277
|
-
def shuffle_positions_on_intermediate_items(old_position, new_position)
|
|
344
|
+
def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
|
|
278
345
|
return if old_position == new_position
|
|
279
|
-
|
|
346
|
+
avoid_id_condition = avoid_id ? " AND #{self.class.primary_key} != #{avoid_id}" : ''
|
|
280
347
|
if old_position < new_position
|
|
281
348
|
# Decrement position of intermediate items
|
|
282
349
|
#
|
|
283
350
|
# e.g., if moving an item from 2 to 5,
|
|
284
351
|
# move [3, 4, 5] to [2, 3, 4]
|
|
285
|
-
acts_as_list_class.
|
|
286
|
-
"#{
|
|
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)"
|
|
287
356
|
)
|
|
288
357
|
else
|
|
289
358
|
# Increment position of intermediate items
|
|
290
359
|
#
|
|
291
360
|
# e.g., if moving an item from 5 to 2,
|
|
292
361
|
# move [2, 3, 4] to [3, 4, 5]
|
|
293
|
-
acts_as_list_class.
|
|
294
|
-
"#{
|
|
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)"
|
|
295
366
|
)
|
|
296
367
|
end
|
|
297
368
|
end
|
|
@@ -304,18 +375,40 @@ module ActiveRecord
|
|
|
304
375
|
else
|
|
305
376
|
increment_positions_on_lower_items(position)
|
|
306
377
|
end
|
|
307
|
-
|
|
378
|
+
set_list_position(position)
|
|
308
379
|
end
|
|
309
380
|
|
|
310
381
|
# used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
|
|
311
382
|
def store_at_0
|
|
312
383
|
if in_list?
|
|
313
384
|
old_position = send(position_column).to_i
|
|
314
|
-
|
|
385
|
+
set_list_position(0)
|
|
315
386
|
decrement_positions_on_lower_items(old_position)
|
|
316
387
|
end
|
|
317
388
|
end
|
|
318
|
-
|
|
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
|
|
319
412
|
end
|
|
320
413
|
end
|
|
321
414
|
end
|
data/lib/acts_as_list/version.rb
CHANGED
data/lib/acts_as_list.rb
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
require 'acts_as_list/active_record/acts/list'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
module ActsAsList
|
|
4
|
+
if defined? Rails::Railtie
|
|
5
|
+
require 'rails'
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer 'acts_as_list.insert_into_active_record' do
|
|
8
|
+
ActiveSupport.on_load :active_record do
|
|
9
|
+
ActsAsList::Railtie.insert
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Railtie
|
|
16
|
+
def self.insert
|
|
17
|
+
if defined?(ActiveRecord)
|
|
18
|
+
ActiveRecord::Base.send(:include, ActiveRecord::Acts::List)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ActsAsList::Railtie.insert
|
data/test/shared.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
|
|
@@ -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
|
|
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
|
|
@@ -102,17 +106,6 @@ module Shared
|
|
|
102
106
|
assert_equal 5, new4.pos
|
|
103
107
|
end
|
|
104
108
|
|
|
105
|
-
def test_insert_middle_with_unsaved_item
|
|
106
|
-
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
107
|
-
|
|
108
|
-
# new item, not saved yet
|
|
109
|
-
insert_this = ListMixin.new(:parent_id => 5)
|
|
110
|
-
insert_this.insert_at(2)
|
|
111
|
-
|
|
112
|
-
assert_equal [1, 2, 3, 4, 5], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:pos)
|
|
113
|
-
assert_equal [1, 5, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
109
|
def test_delete_middle
|
|
117
110
|
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
118
111
|
|
|
@@ -215,14 +208,18 @@ module Shared
|
|
|
215
208
|
def test_before_create_callback_adds_to_given_position
|
|
216
209
|
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
217
210
|
|
|
218
|
-
new = ListMixin.
|
|
211
|
+
new = ListMixin.new(:parent_id => 5)
|
|
212
|
+
new.pos = 1
|
|
213
|
+
new.save!
|
|
219
214
|
assert_equal 1, new.pos
|
|
220
215
|
assert new.first?
|
|
221
216
|
assert !new.last?
|
|
222
217
|
|
|
223
218
|
assert_equal [5, 1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
224
219
|
|
|
225
|
-
new = ListMixin.
|
|
220
|
+
new = ListMixin.new(:parent_id => 5)
|
|
221
|
+
new.pos = 3
|
|
222
|
+
new.save!
|
|
226
223
|
assert_equal 3, new.pos
|
|
227
224
|
assert !new.first?
|
|
228
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
|
|
@@ -52,7 +72,7 @@ module Shared
|
|
|
52
72
|
new = ListMixinSub1.create("parent_id" => 20)
|
|
53
73
|
assert_equal 2, new.pos
|
|
54
74
|
|
|
55
|
-
new =
|
|
75
|
+
new = ListMixinSub1.create("parent_id" => 20)
|
|
56
76
|
assert_equal 3, new.pos
|
|
57
77
|
|
|
58
78
|
new4 = ListMixin.create("parent_id" => 20)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Shared
|
|
2
|
+
module TopAddition
|
|
3
|
+
def setup
|
|
4
|
+
(1..4).each { |counter| TopAdditionMixin.create! :pos => counter, :parent_id => 5 }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def test_reordering
|
|
8
|
+
assert_equal [4, 3, 2, 1], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
9
|
+
|
|
10
|
+
TopAdditionMixin.find(2).move_lower
|
|
11
|
+
assert_equal [4, 3, 1, 2], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
12
|
+
|
|
13
|
+
TopAdditionMixin.find(2).move_higher
|
|
14
|
+
assert_equal [4, 3, 2, 1], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
15
|
+
|
|
16
|
+
TopAdditionMixin.find(1).move_to_bottom
|
|
17
|
+
assert_equal [4, 3, 2, 1], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
18
|
+
|
|
19
|
+
TopAdditionMixin.find(1).move_to_top
|
|
20
|
+
assert_equal [1, 4, 3, 2], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
21
|
+
|
|
22
|
+
TopAdditionMixin.find(2).move_to_bottom
|
|
23
|
+
assert_equal [1, 4, 3, 2], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
24
|
+
|
|
25
|
+
TopAdditionMixin.find(4).move_to_top
|
|
26
|
+
assert_equal [4, 1, 3, 2], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_injection
|
|
30
|
+
item = TopAdditionMixin.new(:parent_id => 1)
|
|
31
|
+
assert_equal '"mixins"."parent_id" = 1', item.scope_condition
|
|
32
|
+
assert_equal "pos", item.position_column
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_insert
|
|
36
|
+
new = TopAdditionMixin.create(:parent_id => 20)
|
|
37
|
+
assert_equal 1, new.pos
|
|
38
|
+
assert new.first?
|
|
39
|
+
assert new.last?
|
|
40
|
+
|
|
41
|
+
new = TopAdditionMixin.create(:parent_id => 20)
|
|
42
|
+
assert_equal 1, new.pos
|
|
43
|
+
assert new.first?
|
|
44
|
+
assert !new.last?
|
|
45
|
+
|
|
46
|
+
new = TopAdditionMixin.create(:parent_id => 20)
|
|
47
|
+
assert_equal 1, new.pos
|
|
48
|
+
assert new.first?
|
|
49
|
+
assert !new.last?
|
|
50
|
+
|
|
51
|
+
new = TopAdditionMixin.create(:parent_id => 0)
|
|
52
|
+
assert_equal 1, new.pos
|
|
53
|
+
assert new.first?
|
|
54
|
+
assert new.last?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_insert_at
|
|
58
|
+
new = TopAdditionMixin.create(:parent_id => 20)
|
|
59
|
+
assert_equal 1, new.pos
|
|
60
|
+
|
|
61
|
+
new = TopAdditionMixin.create(:parent_id => 20)
|
|
62
|
+
assert_equal 1, new.pos
|
|
63
|
+
|
|
64
|
+
new = TopAdditionMixin.create(:parent_id => 20)
|
|
65
|
+
assert_equal 1, new.pos
|
|
66
|
+
|
|
67
|
+
new4 = TopAdditionMixin.create(:parent_id => 20)
|
|
68
|
+
assert_equal 1, new4.pos
|
|
69
|
+
|
|
70
|
+
new4.insert_at(3)
|
|
71
|
+
assert_equal 3, new4.pos
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_delete_middle
|
|
75
|
+
assert_equal [4, 3, 2, 1], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
76
|
+
|
|
77
|
+
TopAdditionMixin.find(2).destroy
|
|
78
|
+
|
|
79
|
+
assert_equal [4, 3, 1], TopAdditionMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
|
80
|
+
|
|
81
|
+
assert_equal 3, TopAdditionMixin.find(1).pos
|
|
82
|
+
assert_equal 2, TopAdditionMixin.find(3).pos
|
|
83
|
+
assert_equal 1, TopAdditionMixin.find(4).pos
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
end
|
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,15 @@ 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
|
+
|
|
97
|
+
class TopAdditionMixin < Mixin
|
|
98
|
+
acts_as_list :column => "pos", :add_new_at => :top, :scope => :parent_id
|
|
99
|
+
end
|
|
100
|
+
|
|
72
101
|
class ActsAsListTestCase < Test::Unit::TestCase
|
|
73
102
|
# No default test required a this class is abstract.
|
|
74
103
|
# Need for test/unit.
|
|
@@ -154,7 +183,7 @@ end
|
|
|
154
183
|
class DefaultScopedTest < ActsAsListTestCase
|
|
155
184
|
def setup
|
|
156
185
|
setup_db
|
|
157
|
-
(1..4).each { |counter| DefaultScopedMixin.create!
|
|
186
|
+
(1..4).each { |counter| DefaultScopedMixin.create!({:pos => counter}) }
|
|
158
187
|
end
|
|
159
188
|
|
|
160
189
|
def test_insert
|
|
@@ -231,4 +260,217 @@ class DefaultScopedTest < ActsAsListTestCase
|
|
|
231
260
|
assert_equal 4, new4.pos
|
|
232
261
|
end
|
|
233
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
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
#class TopAdditionMixin < Mixin
|
|
417
|
+
|
|
418
|
+
class TopAdditionTest < ActsAsListTestCase
|
|
419
|
+
include Shared::TopAddition
|
|
420
|
+
|
|
421
|
+
def setup
|
|
422
|
+
setup_db
|
|
423
|
+
super
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class TopAdditionTestWithDefault < ActsAsListTestCase
|
|
429
|
+
include Shared::TopAddition
|
|
430
|
+
|
|
431
|
+
def setup
|
|
432
|
+
setup_db_with_default
|
|
433
|
+
super
|
|
434
|
+
end
|
|
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
|
+
|
|
234
476
|
end
|
metadata
CHANGED
|
@@ -1,76 +1,98 @@
|
|
|
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: &2152299540 !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
|
|
93
|
+
- .travis.yml
|
|
71
94
|
- Gemfile
|
|
72
95
|
- README.md
|
|
73
|
-
- README.rdoc
|
|
74
96
|
- Rakefile
|
|
75
97
|
- acts_as_list.gemspec
|
|
76
98
|
- init.rb
|
|
@@ -82,37 +104,48 @@ files:
|
|
|
82
104
|
- test/shared_array_scope_list.rb
|
|
83
105
|
- test/shared_list.rb
|
|
84
106
|
- test/shared_list_sub.rb
|
|
107
|
+
- test/shared_top_addition.rb
|
|
85
108
|
- test/shared_zero_based.rb
|
|
86
109
|
- test/test_list.rb
|
|
87
110
|
homepage: http://github.com/swanandp/acts_as_list
|
|
88
111
|
licenses: []
|
|
112
|
+
|
|
89
113
|
post_install_message:
|
|
90
114
|
rdoc_options: []
|
|
91
|
-
|
|
115
|
+
|
|
116
|
+
require_paths:
|
|
92
117
|
- lib
|
|
93
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
119
|
none: false
|
|
95
|
-
requirements:
|
|
96
|
-
- -
|
|
97
|
-
- !ruby/object:Gem::Version
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
100
128
|
none: false
|
|
101
|
-
requirements:
|
|
102
|
-
- -
|
|
103
|
-
- !ruby/object:Gem::Version
|
|
104
|
-
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
hash: 3
|
|
133
|
+
segments:
|
|
134
|
+
- 0
|
|
135
|
+
version: "0"
|
|
105
136
|
requirements: []
|
|
137
|
+
|
|
106
138
|
rubyforge_project: acts_as_list
|
|
107
139
|
rubygems_version: 1.8.15
|
|
108
140
|
signing_key:
|
|
109
141
|
specification_version: 3
|
|
110
142
|
summary: A gem allowing a active_record model to act_as_list.
|
|
111
|
-
test_files:
|
|
143
|
+
test_files:
|
|
112
144
|
- test/helper.rb
|
|
113
145
|
- test/shared.rb
|
|
114
146
|
- test/shared_array_scope_list.rb
|
|
115
147
|
- test/shared_list.rb
|
|
116
148
|
- test/shared_list_sub.rb
|
|
149
|
+
- test/shared_top_addition.rb
|
|
117
150
|
- test/shared_zero_based.rb
|
|
118
151
|
- test/test_list.rb
|
data/README.rdoc
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
= ActsAsList
|
|
2
|
-
|
|
3
|
-
== Description
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
== Example
|
|
9
|
-
|
|
10
|
-
class TodoList < ActiveRecord::Base
|
|
11
|
-
has_many :todo_items, :order => "position"
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class TodoItem < ActiveRecord::Base
|
|
15
|
-
belongs_to :todo_list
|
|
16
|
-
acts_as_list :scope => :todo_list
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
todo_list.first.move_to_bottom
|
|
20
|
-
todo_list.last.move_higher
|
|
21
|
-
|
|
22
|
-
== Contributing to acts_as_list
|
|
23
|
-
|
|
24
|
-
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
|
25
|
-
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
|
26
|
-
* Fork the project
|
|
27
|
-
* Start a feature/bugfix branch
|
|
28
|
-
* Commit and push until you are happy with your contribution
|
|
29
|
-
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
|
30
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
|
31
|
-
|
|
32
|
-
== Copyright
|
|
33
|
-
|
|
34
|
-
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
|
|
35
|
-
|