├── init.rb
├── Rakefile
├── README
├── lib
└── active_record
│ └── acts
│ └── list.rb
└── test
└── list_test.rb
/init.rb:
--------------------------------------------------------------------------------
1 | $:.unshift "#{File.dirname(__FILE__)}/lib"
2 | require 'active_record/acts/list'
3 | ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
4 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake'
2 | require 'rake/testtask'
3 |
4 | desc 'Default: run acts_as_list unit tests.'
5 | task :default => :test
6 |
7 | desc 'Test the acts_as_ordered_tree plugin.'
8 | Rake::TestTask.new(:test) do |t|
9 | t.libs << 'lib'
10 | t.pattern = 'test/**/*_test.rb'
11 | t.verbose = true
12 | end
13 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | ActsAsList
2 | ==========
3 |
4 | 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.
5 |
6 |
7 | Example
8 | =======
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 |
23 | Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
--------------------------------------------------------------------------------
/lib/active_record/acts/list.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | module Acts #:nodoc:
3 | module List #:nodoc:
4 | def self.included(base)
5 | base.extend(ClassMethods)
6 | end
7 |
8 | # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9 | # The class that has this specified needs to have a +position+ column defined as an integer on
10 | # the mapped database table.
11 | #
12 | # Todo list example:
13 | #
14 | # class TodoList < ActiveRecord::Base
15 | # has_many :todo_items, :order => "position"
16 | # end
17 | #
18 | # class TodoItem < ActiveRecord::Base
19 | # belongs_to :todo_list
20 | # acts_as_list :scope => :todo_list
21 | # end
22 | #
23 | # todo_list.first.move_to_bottom
24 | # todo_list.last.move_higher
25 | module ClassMethods
26 | # Configuration options are:
27 | #
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 _id
30 | # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31 | # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32 | # Example: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'
33 | def acts_as_list(options = {})
34 | configuration = { :column => "position", :scope => "1 = 1" }
35 | configuration.update(options) if options.is_a?(Hash)
36 |
37 | configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38 |
39 | if configuration[:scope].is_a?(Symbol)
40 | scope_condition_method = %(
41 | def scope_condition
42 | self.class.send(:sanitize_sql_hash_for_conditions, { :#{configuration[:scope].to_s} => send(:#{configuration[:scope].to_s}) })
43 | end
44 | )
45 | elsif configuration[:scope].is_a?(Array)
46 | scope_condition_method = %(
47 | def scope_condition
48 | attrs = %w(#{configuration[:scope].join(" ")}).inject({}) do |memo,column|
49 | memo[column.intern] = send(column.intern); memo
50 | end
51 | self.class.send(:sanitize_sql_hash_for_conditions, attrs)
52 | end
53 | )
54 | else
55 | scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
56 | end
57 |
58 | class_eval <<-EOV
59 | include ActiveRecord::Acts::List::InstanceMethods
60 |
61 | def acts_as_list_class
62 | ::#{self.name}
63 | end
64 |
65 | def position_column
66 | '#{configuration[:column]}'
67 | end
68 |
69 | #{scope_condition_method}
70 |
71 | before_destroy :decrement_positions_on_lower_items
72 | before_create :add_to_list_bottom
73 | EOV
74 | end
75 | end
76 |
77 | # All the methods available to a record that has had acts_as_list specified. Each method works
78 | # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter
79 | # lower in the list of all chapters. Likewise, chapter.first? would return +true+ if that chapter is
80 | # the first in the list of all chapters.
81 | module InstanceMethods
82 | # Insert the item at the given position (defaults to the top position of 1).
83 | def insert_at(position = 1)
84 | insert_at_position(position)
85 | end
86 |
87 | # Swap positions with the next lower item, if one exists.
88 | def move_lower
89 | return unless lower_item
90 |
91 | acts_as_list_class.transaction do
92 | lower_item.decrement_position
93 | increment_position
94 | end
95 | end
96 |
97 | # Swap positions with the next higher item, if one exists.
98 | def move_higher
99 | return unless higher_item
100 |
101 | acts_as_list_class.transaction do
102 | higher_item.increment_position
103 | decrement_position
104 | end
105 | end
106 |
107 | # Move to the bottom of the list. If the item is already in the list, the items below it have their
108 | # position adjusted accordingly.
109 | def move_to_bottom
110 | return unless in_list?
111 | acts_as_list_class.transaction do
112 | decrement_positions_on_lower_items
113 | assume_bottom_position
114 | end
115 | end
116 |
117 | # Move to the top of the list. If the item is already in the list, the items above it have their
118 | # position adjusted accordingly.
119 | def move_to_top
120 | return unless in_list?
121 | acts_as_list_class.transaction do
122 | increment_positions_on_higher_items
123 | assume_top_position
124 | end
125 | end
126 |
127 | # Removes the item from the list.
128 | def remove_from_list
129 | if in_list?
130 | decrement_positions_on_lower_items
131 | update_attribute position_column, nil
132 | end
133 | end
134 |
135 | # Increase the position of this item without adjusting the rest of the list.
136 | def increment_position
137 | return unless in_list?
138 | update_attribute position_column, self.send(position_column).to_i + 1
139 | end
140 |
141 | # Decrease the position of this item without adjusting the rest of the list.
142 | def decrement_position
143 | return unless in_list?
144 | update_attribute position_column, self.send(position_column).to_i - 1
145 | end
146 |
147 | # Return +true+ if this object is the first in the list.
148 | def first?
149 | return false unless in_list?
150 | self.send(position_column) == 1
151 | end
152 |
153 | # Return +true+ if this object is the last in the list.
154 | def last?
155 | return false unless in_list?
156 | self.send(position_column) == bottom_position_in_list
157 | end
158 |
159 | # Return the next higher item in the list.
160 | def higher_item
161 | return nil unless in_list?
162 | acts_as_list_class.find(:first, :conditions =>
163 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
164 | )
165 | end
166 |
167 | # Return the next lower item in the list.
168 | def lower_item
169 | return nil unless in_list?
170 | acts_as_list_class.find(:first, :conditions =>
171 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
172 | )
173 | end
174 |
175 | # Test if this record is in a list
176 | def in_list?
177 | !send(position_column).nil?
178 | end
179 |
180 | private
181 | def add_to_list_top
182 | increment_positions_on_all_items
183 | end
184 |
185 | def add_to_list_bottom
186 | self[position_column] = bottom_position_in_list.to_i + 1
187 | end
188 |
189 | # Overwrite this method to define the scope of the list changes
190 | def scope_condition() "1" end
191 |
192 | # Returns the bottom position number in the list.
193 | # bottom_position_in_list # => 2
194 | def bottom_position_in_list(except = nil)
195 | item = bottom_item(except)
196 | item ? item.send(position_column) : 0
197 | end
198 |
199 | # Returns the bottom item
200 | def bottom_item(except = nil)
201 | conditions = scope_condition
202 | conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
203 | acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
204 | end
205 |
206 | # Forces item to assume the bottom position in the list.
207 | def assume_bottom_position
208 | update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
209 | end
210 |
211 | # Forces item to assume the top position in the list.
212 | def assume_top_position
213 | update_attribute(position_column, 1)
214 | end
215 |
216 | # This has the effect of moving all the higher items up one.
217 | def decrement_positions_on_higher_items(position)
218 | acts_as_list_class.update_all(
219 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
220 | )
221 | end
222 |
223 | # This has the effect of moving all the lower items up one.
224 | def decrement_positions_on_lower_items
225 | return unless in_list?
226 | acts_as_list_class.update_all(
227 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
228 | )
229 | end
230 |
231 | # This has the effect of moving all the higher items down one.
232 | def increment_positions_on_higher_items
233 | return unless in_list?
234 | acts_as_list_class.update_all(
235 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
236 | )
237 | end
238 |
239 | # This has the effect of moving all the lower items down one.
240 | def increment_positions_on_lower_items(position)
241 | acts_as_list_class.update_all(
242 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
243 | )
244 | end
245 |
246 | # Increments position (position_column) of all items in the list.
247 | def increment_positions_on_all_items
248 | acts_as_list_class.update_all(
249 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
250 | )
251 | end
252 |
253 | def insert_at_position(position)
254 | remove_from_list
255 | increment_positions_on_lower_items(position)
256 | self.update_attribute(position_column, position)
257 | end
258 | end
259 | end
260 | end
261 | end
262 |
--------------------------------------------------------------------------------
/test/list_test.rb:
--------------------------------------------------------------------------------
1 | require 'test/unit'
2 |
3 | require 'rubygems'
4 | gem 'activerecord', '>= 1.15.4.7794'
5 | require 'active_record'
6 |
7 | require "#{File.dirname(__FILE__)}/../init"
8 |
9 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
10 |
11 | def setup_db
12 | ActiveRecord::Schema.define(:version => 1) do
13 | create_table :mixins do |t|
14 | t.column :pos, :integer
15 | t.column :parent_id, :integer
16 | t.column :parent_type, :string
17 | t.column :created_at, :datetime
18 | t.column :updated_at, :datetime
19 | end
20 | end
21 | end
22 |
23 | def teardown_db
24 | ActiveRecord::Base.connection.tables.each do |table|
25 | ActiveRecord::Base.connection.drop_table(table)
26 | end
27 | end
28 |
29 | class Mixin < ActiveRecord::Base
30 | end
31 |
32 | class ListMixin < Mixin
33 | acts_as_list :column => "pos", :scope => :parent
34 |
35 | def self.table_name() "mixins" end
36 | end
37 |
38 | class ListMixinSub1 < ListMixin
39 | end
40 |
41 | class ListMixinSub2 < ListMixin
42 | end
43 |
44 | class ListWithStringScopeMixin < ActiveRecord::Base
45 | acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
46 |
47 | def self.table_name() "mixins" end
48 | end
49 |
50 | class ArrayScopeListMixin < Mixin
51 | acts_as_list :column => "pos", :scope => [:parent_id, :parent_type]
52 |
53 | def self.table_name() "mixins" end
54 | end
55 |
56 | class ListTest < Test::Unit::TestCase
57 |
58 | def setup
59 | setup_db
60 | (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
61 | end
62 |
63 | def teardown
64 | teardown_db
65 | end
66 |
67 | def test_reordering
68 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
69 |
70 | ListMixin.find(2).move_lower
71 | assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
72 |
73 | ListMixin.find(2).move_higher
74 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
75 |
76 | ListMixin.find(1).move_to_bottom
77 | assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
78 |
79 | ListMixin.find(1).move_to_top
80 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
81 |
82 | ListMixin.find(2).move_to_bottom
83 | assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
84 |
85 | ListMixin.find(4).move_to_top
86 | assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
87 | end
88 |
89 | def test_move_to_bottom_with_next_to_last_item
90 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
91 | ListMixin.find(3).move_to_bottom
92 | assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
93 | end
94 |
95 | def test_next_prev
96 | assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
97 | assert_nil ListMixin.find(1).higher_item
98 | assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
99 | assert_nil ListMixin.find(4).lower_item
100 | end
101 |
102 | def test_injection
103 | item = ListMixin.new(:parent_id => 1)
104 | assert_equal '"mixins"."parent_id" = 1', item.scope_condition
105 | assert_equal "pos", item.position_column
106 | end
107 |
108 | def test_insert
109 | new = ListMixin.create(:parent_id => 20)
110 | assert_equal 1, new.pos
111 | assert new.first?
112 | assert new.last?
113 |
114 | new = ListMixin.create(:parent_id => 20)
115 | assert_equal 2, new.pos
116 | assert !new.first?
117 | assert new.last?
118 |
119 | new = ListMixin.create(:parent_id => 20)
120 | assert_equal 3, new.pos
121 | assert !new.first?
122 | assert new.last?
123 |
124 | new = ListMixin.create(:parent_id => 0)
125 | assert_equal 1, new.pos
126 | assert new.first?
127 | assert new.last?
128 | end
129 |
130 | def test_insert_at
131 | new = ListMixin.create(:parent_id => 20)
132 | assert_equal 1, new.pos
133 |
134 | new = ListMixin.create(:parent_id => 20)
135 | assert_equal 2, new.pos
136 |
137 | new = ListMixin.create(:parent_id => 20)
138 | assert_equal 3, new.pos
139 |
140 | new4 = ListMixin.create(:parent_id => 20)
141 | assert_equal 4, new4.pos
142 |
143 | new4.insert_at(3)
144 | assert_equal 3, new4.pos
145 |
146 | new.reload
147 | assert_equal 4, new.pos
148 |
149 | new.insert_at(2)
150 | assert_equal 2, new.pos
151 |
152 | new4.reload
153 | assert_equal 4, new4.pos
154 |
155 | new5 = ListMixin.create(:parent_id => 20)
156 | assert_equal 5, new5.pos
157 |
158 | new5.insert_at(1)
159 | assert_equal 1, new5.pos
160 |
161 | new4.reload
162 | assert_equal 5, new4.pos
163 | end
164 |
165 | def test_delete_middle
166 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
167 |
168 | ListMixin.find(2).destroy
169 |
170 | assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
171 |
172 | assert_equal 1, ListMixin.find(1).pos
173 | assert_equal 2, ListMixin.find(3).pos
174 | assert_equal 3, ListMixin.find(4).pos
175 |
176 | ListMixin.find(1).destroy
177 |
178 | assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
179 |
180 | assert_equal 1, ListMixin.find(3).pos
181 | assert_equal 2, ListMixin.find(4).pos
182 | end
183 |
184 | def test_with_string_based_scope
185 | new = ListWithStringScopeMixin.create(:parent_id => 500)
186 | assert_equal 1, new.pos
187 | assert new.first?
188 | assert new.last?
189 | end
190 |
191 | def test_nil_scope
192 | new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
193 | new2.move_higher
194 | assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
195 | end
196 |
197 | def test_remove_from_list_should_then_fail_in_list?
198 | assert_equal true, ListMixin.find(1).in_list?
199 | ListMixin.find(1).remove_from_list
200 | assert_equal false, ListMixin.find(1).in_list?
201 | end
202 |
203 | def test_remove_from_list_should_set_position_to_nil
204 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
205 |
206 | ListMixin.find(2).remove_from_list
207 |
208 | assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
209 |
210 | assert_equal 1, ListMixin.find(1).pos
211 | assert_equal nil, ListMixin.find(2).pos
212 | assert_equal 2, ListMixin.find(3).pos
213 | assert_equal 3, ListMixin.find(4).pos
214 | end
215 |
216 | def test_remove_before_destroy_does_not_shift_lower_items_twice
217 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
218 |
219 | ListMixin.find(2).remove_from_list
220 | ListMixin.find(2).destroy
221 |
222 | assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
223 |
224 | assert_equal 1, ListMixin.find(1).pos
225 | assert_equal 2, ListMixin.find(3).pos
226 | assert_equal 3, ListMixin.find(4).pos
227 | end
228 |
229 | def test_before_destroy_callbacks_do_not_update_position_to_nil_before_deleting_the_record
230 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
231 |
232 | # We need to trigger all the before_destroy callbacks without actually
233 | # destroying the record so we can see the affect the callbacks have on
234 | # the record.
235 | list = ListMixin.find(2)
236 | if list.respond_to?(:run_callbacks)
237 | list.run_callbacks(:destroy)
238 | else
239 | list.send(:callback, :before_destroy)
240 | end
241 |
242 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
243 |
244 | assert_equal 1, ListMixin.find(1).pos
245 | assert_equal 2, ListMixin.find(2).pos
246 | assert_equal 2, ListMixin.find(3).pos
247 | assert_equal 3, ListMixin.find(4).pos
248 | end
249 |
250 | end
251 |
252 | class ListSubTest < Test::Unit::TestCase
253 |
254 | def setup
255 | setup_db
256 | (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
257 | end
258 |
259 | def teardown
260 | teardown_db
261 | end
262 |
263 | def test_reordering
264 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
265 |
266 | ListMixin.find(2).move_lower
267 | assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
268 |
269 | ListMixin.find(2).move_higher
270 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
271 |
272 | ListMixin.find(1).move_to_bottom
273 | assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
274 |
275 | ListMixin.find(1).move_to_top
276 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
277 |
278 | ListMixin.find(2).move_to_bottom
279 | assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
280 |
281 | ListMixin.find(4).move_to_top
282 | assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
283 | end
284 |
285 | def test_move_to_bottom_with_next_to_last_item
286 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
287 | ListMixin.find(3).move_to_bottom
288 | assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
289 | end
290 |
291 | def test_next_prev
292 | assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
293 | assert_nil ListMixin.find(1).higher_item
294 | assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
295 | assert_nil ListMixin.find(4).lower_item
296 | end
297 |
298 | def test_injection
299 | item = ListMixin.new("parent_id"=>1)
300 | assert_equal '"mixins"."parent_id" = 1', item.scope_condition
301 | assert_equal "pos", item.position_column
302 | end
303 |
304 | def test_insert_at
305 | new = ListMixin.create("parent_id" => 20)
306 | assert_equal 1, new.pos
307 |
308 | new = ListMixinSub1.create("parent_id" => 20)
309 | assert_equal 2, new.pos
310 |
311 | new = ListMixinSub2.create("parent_id" => 20)
312 | assert_equal 3, new.pos
313 |
314 | new4 = ListMixin.create("parent_id" => 20)
315 | assert_equal 4, new4.pos
316 |
317 | new4.insert_at(3)
318 | assert_equal 3, new4.pos
319 |
320 | new.reload
321 | assert_equal 4, new.pos
322 |
323 | new.insert_at(2)
324 | assert_equal 2, new.pos
325 |
326 | new4.reload
327 | assert_equal 4, new4.pos
328 |
329 | new5 = ListMixinSub1.create("parent_id" => 20)
330 | assert_equal 5, new5.pos
331 |
332 | new5.insert_at(1)
333 | assert_equal 1, new5.pos
334 |
335 | new4.reload
336 | assert_equal 5, new4.pos
337 | end
338 |
339 | def test_delete_middle
340 | assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
341 |
342 | ListMixin.find(2).destroy
343 |
344 | assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
345 |
346 | assert_equal 1, ListMixin.find(1).pos
347 | assert_equal 2, ListMixin.find(3).pos
348 | assert_equal 3, ListMixin.find(4).pos
349 |
350 | ListMixin.find(1).destroy
351 |
352 | assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
353 |
354 | assert_equal 1, ListMixin.find(3).pos
355 | assert_equal 2, ListMixin.find(4).pos
356 | end
357 |
358 | end
359 |
360 | class ArrayScopeListTest < Test::Unit::TestCase
361 |
362 | def setup
363 | setup_db
364 | (1..4).each { |counter| ArrayScopeListMixin.create! :pos => counter, :parent_id => 5, :parent_type => 'ParentClass' }
365 | end
366 |
367 | def teardown
368 | teardown_db
369 | end
370 |
371 | def test_reordering
372 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
373 |
374 | ArrayScopeListMixin.find(2).move_lower
375 | assert_equal [1, 3, 2, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
376 |
377 | ArrayScopeListMixin.find(2).move_higher
378 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
379 |
380 | ArrayScopeListMixin.find(1).move_to_bottom
381 | assert_equal [2, 3, 4, 1], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
382 |
383 | ArrayScopeListMixin.find(1).move_to_top
384 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
385 |
386 | ArrayScopeListMixin.find(2).move_to_bottom
387 | assert_equal [1, 3, 4, 2], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
388 |
389 | ArrayScopeListMixin.find(4).move_to_top
390 | assert_equal [4, 1, 3, 2], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
391 | end
392 |
393 | def test_move_to_bottom_with_next_to_last_item
394 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
395 | ArrayScopeListMixin.find(3).move_to_bottom
396 | assert_equal [1, 2, 4, 3], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
397 | end
398 |
399 | def test_next_prev
400 | assert_equal ArrayScopeListMixin.find(2), ArrayScopeListMixin.find(1).lower_item
401 | assert_nil ArrayScopeListMixin.find(1).higher_item
402 | assert_equal ArrayScopeListMixin.find(3), ArrayScopeListMixin.find(4).higher_item
403 | assert_nil ArrayScopeListMixin.find(4).lower_item
404 | end
405 |
406 | def test_injection
407 | item = ArrayScopeListMixin.new(:parent_id => 1, :parent_type => 'ParentClass')
408 | assert_equal '"mixins"."parent_id" = 1 AND "mixins"."parent_type" = \'ParentClass\'', item.scope_condition
409 | assert_equal "pos", item.position_column
410 | end
411 |
412 | def test_insert
413 | new = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
414 | assert_equal 1, new.pos
415 | assert new.first?
416 | assert new.last?
417 |
418 | new = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
419 | assert_equal 2, new.pos
420 | assert !new.first?
421 | assert new.last?
422 |
423 | new = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
424 | assert_equal 3, new.pos
425 | assert !new.first?
426 | assert new.last?
427 |
428 | new = ArrayScopeListMixin.create(:parent_id => 0, :parent_type => 'ParentClass')
429 | assert_equal 1, new.pos
430 | assert new.first?
431 | assert new.last?
432 | end
433 |
434 | def test_insert_at
435 | new = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
436 | assert_equal 1, new.pos
437 |
438 | new = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
439 | assert_equal 2, new.pos
440 |
441 | new = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
442 | assert_equal 3, new.pos
443 |
444 | new4 = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
445 | assert_equal 4, new4.pos
446 |
447 | new4.insert_at(3)
448 | assert_equal 3, new4.pos
449 |
450 | new.reload
451 | assert_equal 4, new.pos
452 |
453 | new.insert_at(2)
454 | assert_equal 2, new.pos
455 |
456 | new4.reload
457 | assert_equal 4, new4.pos
458 |
459 | new5 = ArrayScopeListMixin.create(:parent_id => 20, :parent_type => 'ParentClass')
460 | assert_equal 5, new5.pos
461 |
462 | new5.insert_at(1)
463 | assert_equal 1, new5.pos
464 |
465 | new4.reload
466 | assert_equal 5, new4.pos
467 | end
468 |
469 | def test_delete_middle
470 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
471 |
472 | ArrayScopeListMixin.find(2).destroy
473 |
474 | assert_equal [1, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
475 |
476 | assert_equal 1, ArrayScopeListMixin.find(1).pos
477 | assert_equal 2, ArrayScopeListMixin.find(3).pos
478 | assert_equal 3, ArrayScopeListMixin.find(4).pos
479 |
480 | ArrayScopeListMixin.find(1).destroy
481 |
482 | assert_equal [3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
483 |
484 | assert_equal 1, ArrayScopeListMixin.find(3).pos
485 | assert_equal 2, ArrayScopeListMixin.find(4).pos
486 | end
487 |
488 | def test_remove_from_list_should_then_fail_in_list?
489 | assert_equal true, ArrayScopeListMixin.find(1).in_list?
490 | ArrayScopeListMixin.find(1).remove_from_list
491 | assert_equal false, ArrayScopeListMixin.find(1).in_list?
492 | end
493 |
494 | def test_remove_from_list_should_set_position_to_nil
495 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
496 |
497 | ArrayScopeListMixin.find(2).remove_from_list
498 |
499 | assert_equal [2, 1, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
500 |
501 | assert_equal 1, ArrayScopeListMixin.find(1).pos
502 | assert_equal nil, ArrayScopeListMixin.find(2).pos
503 | assert_equal 2, ArrayScopeListMixin.find(3).pos
504 | assert_equal 3, ArrayScopeListMixin.find(4).pos
505 | end
506 |
507 | def test_remove_before_destroy_does_not_shift_lower_items_twice
508 | assert_equal [1, 2, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
509 |
510 | ArrayScopeListMixin.find(2).remove_from_list
511 | ArrayScopeListMixin.find(2).destroy
512 |
513 | assert_equal [1, 3, 4], ArrayScopeListMixin.find(:all, :conditions => "parent_id = 5 AND parent_type = 'ParentClass'", :order => 'pos').map(&:id)
514 |
515 | assert_equal 1, ArrayScopeListMixin.find(1).pos
516 | assert_equal 2, ArrayScopeListMixin.find(3).pos
517 | assert_equal 3, ArrayScopeListMixin.find(4).pos
518 | end
519 |
520 | end
521 |
522 |
--------------------------------------------------------------------------------