acts_as_list_ar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ module ActsAsListAR
2
+ def scope_name
3
+ "named_scope"
4
+ end
5
+
6
+ module ClassMethods
7
+ # nothing here, folks
8
+ end
9
+
10
+ module InstanceMethods
11
+ # Return the next higher item in the list.
12
+ def higher_item
13
+ return nil unless in_list?
14
+ acts_as_list_class.find(:first, :conditions =>
15
+ "#{scope_condition} AND #{position_column} < #{send(position_column).to_s}",
16
+ :order => "#{position_column} DESC"
17
+ )
18
+ end
19
+
20
+ # Return the next lower item in the list.
21
+ def lower_item
22
+ return nil unless in_list?
23
+ acts_as_list_class.find(:first, :conditions =>
24
+ "#{scope_condition} AND #{position_column} > #{send(position_column).to_s}",
25
+ :order => "#{position_column} ASC"
26
+ )
27
+ end
28
+
29
+ private
30
+ # Returns the bottom item
31
+ def bottom_item(except = nil)
32
+ conditions = scope_condition
33
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
34
+ acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
35
+ end
36
+
37
+ # This has the effect of moving all the higher items up one.
38
+ def decrement_positions_on_higher_items(position)
39
+ acts_as_list_class.update_all(
40
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
41
+ )
42
+ end
43
+
44
+ # This has the effect of moving all the lower items up one.
45
+ def decrement_positions_on_lower_items
46
+ return unless in_list?
47
+ acts_as_list_class.update_all(
48
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
49
+ )
50
+ end
51
+
52
+ # This has the effect of moving all the higher items down one.
53
+ def increment_positions_on_higher_items
54
+ return unless in_list?
55
+ acts_as_list_class.update_all(
56
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
57
+ )
58
+ end
59
+
60
+ # This has the effect of moving all the lower items down one.
61
+ def increment_positions_on_lower_items(position)
62
+ acts_as_list_class.update_all(
63
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
64
+ )
65
+ end
66
+
67
+ # Increments position (<tt>position_column</tt>) of all items in the list.
68
+ def increment_positions_on_all_items
69
+ acts_as_list_class.update_all(
70
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
71
+ )
72
+ end
73
+ end
74
+
75
+ def self.included(receiver)
76
+ receiver.extend ClassMethods
77
+ receiver.send :include, InstanceMethods
78
+ end
79
+ end
@@ -0,0 +1,282 @@
1
+ module ActsAsListAR
2
+ def scope_name
3
+ "scope"
4
+ end
5
+
6
+ module ClassMethods
7
+ # nothing here, folks
8
+ end
9
+
10
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
11
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
12
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
13
+ # the first in the list of all chapters.
14
+ module InstanceMethods
15
+ # Insert the item at the given position (defaults to the top position of 1).
16
+ def insert_at(position = 1)
17
+ insert_at_position(position)
18
+ end
19
+
20
+ # Inserts the item at the bottom of the list
21
+ def insert_at_bottom
22
+ assume_bottom_position
23
+ end
24
+
25
+ def insert_into(list, options = {})
26
+ position = options.delete(:position) || list.size
27
+ options.each { |attr, value| self.send "#{attr}=", value }
28
+
29
+ list.insert(position, self)
30
+ list.each_with_index { |item, index| item.update_attribute(:position, index + 1) }
31
+ end
32
+
33
+ def move_to(position = 1)
34
+ move_to_position(position)
35
+ end
36
+
37
+ def remove_from(list, options = {})
38
+ if in_list?
39
+ decrement_positions_on_lower_items
40
+
41
+ self.send "#{position_column}=", nil
42
+ options.each { |attr, value| self.send "#{attr}=", value }
43
+ list.delete self
44
+ end
45
+ end
46
+
47
+ # Swap positions with the next lower item, if one exists.
48
+ def move_lower
49
+ lower = lower_item
50
+ return unless lower
51
+ acts_as_list_class.transaction do
52
+ self.update_attribute(position_column, lower.send(position_column))
53
+ lower.decrement_position
54
+ end
55
+ end
56
+
57
+ # Swap positions with the next higher item, if one exists.
58
+ def move_higher
59
+ higher = higher_item
60
+ return unless higher
61
+ acts_as_list_class.transaction do
62
+ self.update_attribute(position_column, higher.send(position_column))
63
+ higher.increment_position
64
+ end
65
+ end
66
+
67
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
68
+ # position adjusted accordingly.
69
+ def move_to_bottom
70
+ # return unless in_list?
71
+ acts_as_list_class.transaction do
72
+ decrement_positions_on_lower_items if in_list?
73
+ assume_bottom_position
74
+ end
75
+ end
76
+
77
+ # Move to the top of the list. If the item is already in the list, the items above it have their
78
+ # position adjusted accordingly.
79
+ def move_to_top
80
+ # return unless in_list?
81
+ acts_as_list_class.transaction do
82
+ # increment_positions_on_higher_items
83
+ in_list? ? increment_positions_on_higher_items : increment_positions_on_all_items
84
+ assume_top_position
85
+ end
86
+ end
87
+
88
+ # Removes the item from the list.
89
+ def remove_from_list
90
+ # if in_list?
91
+ # decrement_positions_on_lower_items
92
+ # update_attribute position_column, nil
93
+ # end
94
+ return unless in_list?
95
+ decrement_positions_on_lower_items
96
+ update_attribute position_column, nil
97
+ end
98
+
99
+ # Increase the position of this item without adjusting the rest of the list.
100
+ def increment_position
101
+ # return unless in_list?
102
+ update_attribute position_column, self.send(position_column).to_i + 1
103
+ end
104
+
105
+ # Decrease the position of this item without adjusting the rest of the list.
106
+ def decrement_position
107
+ # return unless in_list?
108
+ update_attribute position_column, self.send(position_column).to_i - 1
109
+ end
110
+
111
+ # Return +true+ if this object is the first in the list.
112
+ def first?
113
+ # return false unless in_list?
114
+ self.send(position_column) == 1
115
+ end
116
+
117
+ # Return +true+ if this object is the last in the list.
118
+ def last?
119
+ # return false unless in_list?
120
+ self.send(position_column) == bottom_position_in_list
121
+ end
122
+
123
+ # Return the next higher item in the list.
124
+ def higher_item
125
+ # return nil unless in_list? # http://github.com/brightspark3/acts_as_list/commit/8e55352aaa437d23a1ebdeabd5276c6dd5aad6a1
126
+
127
+ # acts_as_list_class.find(:first, :conditions =>
128
+ # "#{scope_condition} AND #{position_column} < #{send(position_column).to_s}", :order => "#{position_column} DESC"
129
+ # )
130
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
131
+ where("#{position_column} = #{(send(position_column).to_i - 1).to_s}").first
132
+ end
133
+ end
134
+
135
+ # Return the next lower item in the list.
136
+ def lower_item
137
+ # return nil unless in_list?
138
+ # acts_as_list_class.find(:first, :conditions => "#{scope_condition} AND #{position_column} > #{send(position_column).to_s}", :order => "#{position_column} ASC")
139
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
140
+ where("#{position_column} = #{(send(position_column).to_i + 1).to_s}").first
141
+ end
142
+ end
143
+
144
+ # Test if this record is in a list
145
+ def in_list?
146
+ !send(position_column).nil?
147
+ end
148
+
149
+ private
150
+ def add_to_list_top
151
+ increment_positions_on_all_items
152
+ end
153
+
154
+ def add_to_list_bottom_when_necessary
155
+ self[position_column] = bottom_position_in_list.to_i + 1 if send(position_column).nil?
156
+ end
157
+
158
+ def add_to_list_bottom
159
+ self[position_column] = bottom_position_in_list.to_i + 1
160
+ end
161
+
162
+ # Overwrite this method to define the scope of the list changes
163
+ def scope_condition() "1" end
164
+
165
+ # Returns the bottom position number in the list.
166
+ # bottom_position_in_list # => 2
167
+ def bottom_position_in_list(except = nil)
168
+ item = bottom_item(except)
169
+ item ? item.send(position_column) : 0
170
+ end
171
+
172
+ # Returns the bottom item
173
+ def bottom_item(except = nil)
174
+ # conditions = scope_condition
175
+ # conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
176
+ # acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
177
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
178
+ conditions = except.nil?() ? "" : "#{self.class.primary_key} != #{except.id}"
179
+ where(conditions).order("#{position_column} DESC").first
180
+ end
181
+ end
182
+
183
+ # Forces item to assume the bottom position in the list.
184
+ def assume_bottom_position
185
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
186
+ end
187
+
188
+ # Forces item to assume the top position in the list.
189
+ def assume_top_position
190
+ update_attribute(position_column, 1)
191
+ end
192
+
193
+ # This has the effect of moving all the higher items up one.
194
+ def decrement_positions_on_higher_items(position)
195
+ # acts_as_list_class.update_all("#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}")
196
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
197
+ update_all("#{position_column} = (#{position_column} - 1)", "#{position_column} <= #{position}")
198
+ end
199
+ end
200
+
201
+ # This has the effect of moving all the lower items up one.
202
+ def decrement_positions_on_lower_items
203
+ # return unless in_list?
204
+ # acts_as_list_class.update_all("#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}")
205
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
206
+ update_all("#{position_column} = (#{position_column} - 1)", "#{position_column} > #{send(position_column).to_i}")
207
+ end
208
+ end
209
+
210
+ # This has the effect of moving all the higher items down one.
211
+ def increment_positions_on_higher_items
212
+ # return unless in_list?
213
+ # acts_as_list_class.update_all("#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}")
214
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
215
+ update_all("#{position_column} = (#{position_column} + 1)", "#{position_column} < #{send(position_column).to_i}")
216
+ end
217
+ end
218
+
219
+ # This has the effect of moving all the lower items down one.
220
+ def increment_positions_on_lower_items(position)
221
+ # acts_as_list_class.update_all("#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}")
222
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
223
+ update_all("#{position_column} = (#{position_column} + 1)", "#{position_column} >= #{position}")
224
+ end
225
+ end
226
+
227
+ # Increments position (<tt>position_column</tt>) of all items in the list.
228
+ def increment_positions_on_all_items
229
+ # acts_as_list_class.update_all("#{position_column} = (#{position_column} + 1)", "#{scope_condition}")
230
+ acts_as_list_class.with_acts_as_list_scope(scope_condition) do
231
+ update_all("#{position_column} = (#{position_column} + 1)")
232
+ end
233
+ end
234
+
235
+ def insert_at_position(position)
236
+ remove_from_list
237
+ increment_positions_on_lower_items(position)
238
+ self.update_attribute(position_column, position)
239
+ end
240
+
241
+
242
+ # This has the effect of moving all items between two positions (inclusive) up one.
243
+ def decrement_positions_between(low, high)
244
+ acts_as_list_class.update_all(
245
+ "#{position_column} = (#{position_column} - 1)", ["#{scope_condition} AND #{position_column} >= ? AND #{position_column} <= ?", low, high]
246
+ )
247
+ end
248
+
249
+ # This has the effect of moving all items between two positions (inclusive) down one.
250
+ def increment_positions_between(low, high)
251
+ acts_as_list_class.update_all(
252
+ "#{position_column} = (#{position_column} + 1)", ["#{scope_condition} AND #{position_column} >= ? AND #{position_column} <= ?", low, high]
253
+ )
254
+ end
255
+
256
+ # Moves an existing list element to the "new_position" slot.
257
+ def move_to_position(new_position)
258
+ old_position = self.send(position_column)
259
+ unless new_position == old_position
260
+ if new_position < old_position
261
+ # Moving higher in the list (up)
262
+ new_position = [1, new_position].max
263
+ increment_positions_between(new_position, old_position - 1)
264
+ else
265
+ # Moving lower in the list (down)
266
+ new_position = [bottom_position_in_list(self).to_i, new_position].min
267
+ decrement_positions_between(old_position + 1, new_position)
268
+ end
269
+ self.update_attribute(position_column, new_position)
270
+ end
271
+ end
272
+
273
+ def eliminate_current_position
274
+ decrement_positions_on_lower_items if in_list?
275
+ end
276
+ end
277
+
278
+ def self.included(receiver)
279
+ receiver.extend ClassMethods
280
+ receiver.send :include, InstanceMethods
281
+ end
282
+ end
@@ -0,0 +1,112 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class ListTest
4
+ class << self
5
+ def before_destroy(*attr); end
6
+ def before_create(*attr); end
7
+ end
8
+
9
+ include ActiveRecord::Acts::List
10
+ acts_as_list :scope => :parent
11
+
12
+ attr_accessor :position, :attr1, :attr2
13
+ end
14
+
15
+ describe ActiveRecord::Acts::List do
16
+
17
+ before do
18
+ @elem = ListTest.new
19
+ @list = []
20
+ end
21
+
22
+ describe "insert at bottom" do
23
+ it "should insert the element at the bottom of the list" do
24
+ @elem.should_receive :assume_bottom_position
25
+ @elem.insert_at_bottom
26
+ end
27
+ end
28
+
29
+ describe "insert into" do
30
+ before do
31
+ @elem.stub! :update_attribute
32
+ end
33
+
34
+ it "should insert the element at the bottom" do
35
+ @list.should_receive(:insert).with(0, @elem)
36
+ @elem.insert_into @list
37
+ end
38
+
39
+ it "should set all the optional attributes to the given value" do
40
+ @elem.insert_into @list, :attr1 => "value1", :attr2 => "value2"
41
+ @elem.attr1.should == "value1"
42
+ @elem.attr2.should == "value2"
43
+ end
44
+
45
+ it "should insert into a specified position" do
46
+ @list.should_receive(:insert).with(41, @elem)
47
+ @elem.insert_into @list, :position => 41
48
+ end
49
+
50
+ it "should update the position on all attributes in the list" do
51
+ @elem2 = ListTest.new
52
+ @list = [@elem2]
53
+
54
+ @elem.should_receive(:update_attribute).with(:position, 2)
55
+ @elem2.should_receive(:update_attribute).with(:position, 1)
56
+
57
+ @elem.insert_into @list, :position => 1
58
+ end
59
+ end
60
+
61
+ describe "remove from" do
62
+ it "should not do anything if the elem is not in the list" do
63
+ @elem.should_not_receive :decrement_positions_on_lower_items
64
+ @elem.remove_from @list
65
+ end
66
+
67
+ describe "in list" do
68
+ before do
69
+ @list = [@elem]
70
+ @elem.position = 1
71
+ @elem.stub! :decrement_positions_on_lower_items
72
+ end
73
+
74
+ it "should decrement the position on all lower items" do
75
+ @elem.should_receive :decrement_positions_on_lower_items
76
+ @elem.remove_from @list
77
+ end
78
+
79
+ it "should set the position to nil" do
80
+ @elem.remove_from @list
81
+ @elem.position.should be_nil
82
+ end
83
+
84
+ it "should delete the element from the list" do
85
+ @elem.remove_from @list
86
+ @list.should_not include(@elem)
87
+ end
88
+
89
+ it "should set all the optional attributes to the given value" do
90
+ @elem.remove_from @list, :attr1 => "unset1", :attr2 => "unset2"
91
+ @elem.attr1.should == "unset1"
92
+ @elem.attr2.should == "unset2"
93
+ end
94
+ end
95
+ end
96
+
97
+ { "higher_items" => :<, "lower_items" => :> }.each do |items, operator|
98
+ describe items do
99
+ it "should return nil if the element is not in the list" do
100
+ @elem.send(items).should be_nil
101
+ end
102
+
103
+ it "should find all #{items} with a ActiveRecord finder" do
104
+ @elem.position = 23
105
+ @elem.stub!(:scope_condition).and_return "scope"
106
+ ListTest.should_receive(:find).with :all, :conditions => "scope AND position #{operator} 23"
107
+
108
+ @elem.send items
109
+ end
110
+ end
111
+ end
112
+ end