Major update of Selectricity to work with Rails 2.2.2 from 1.2!
[selectricity] / vendor / plugins / acts_as_list / 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 <tt>_id</tt> 
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: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
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                 if #{configuration[:scope].to_s}.nil?
43                   "#{configuration[:scope].to_s} IS NULL"
44                 else
45                   "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46                 end
47               end
48             )
49           else
50             scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51           end
52
53           class_eval <<-EOV
54             include ActiveRecord::Acts::List::InstanceMethods
55
56             def acts_as_list_class
57               ::#{self.name}
58             end
59
60             def position_column
61               '#{configuration[:column]}'
62             end
63
64             #{scope_condition_method}
65
66             before_destroy :remove_from_list
67             before_create  :add_to_list_bottom
68           EOV
69         end
70       end
71
72       # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73       # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74       # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75       # the first in the list of all chapters.
76       module InstanceMethods
77         # Insert the item at the given position (defaults to the top position of 1).
78         def insert_at(position = 1)
79           insert_at_position(position)
80         end
81
82         # Swap positions with the next lower item, if one exists.
83         def move_lower
84           return unless lower_item
85
86           acts_as_list_class.transaction do
87             lower_item.decrement_position
88             increment_position
89           end
90         end
91
92         # Swap positions with the next higher item, if one exists.
93         def move_higher
94           return unless higher_item
95
96           acts_as_list_class.transaction do
97             higher_item.increment_position
98             decrement_position
99           end
100         end
101
102         # Move to the bottom of the list. If the item is already in the list, the items below it have their
103         # position adjusted accordingly.
104         def move_to_bottom
105           return unless in_list?
106           acts_as_list_class.transaction do
107             decrement_positions_on_lower_items
108             assume_bottom_position
109           end
110         end
111
112         # Move to the top of the list. If the item is already in the list, the items above it have their
113         # position adjusted accordingly.
114         def move_to_top
115           return unless in_list?
116           acts_as_list_class.transaction do
117             increment_positions_on_higher_items
118             assume_top_position
119           end
120         end
121
122         # Removes the item from the list.
123         def remove_from_list
124           if in_list?
125             decrement_positions_on_lower_items
126             update_attribute position_column, nil
127           end
128         end
129
130         # Increase the position of this item without adjusting the rest of the list.
131         def increment_position
132           return unless in_list?
133           update_attribute position_column, self.send(position_column).to_i + 1
134         end
135
136         # Decrease the position of this item without adjusting the rest of the list.
137         def decrement_position
138           return unless in_list?
139           update_attribute position_column, self.send(position_column).to_i - 1
140         end
141
142         # Return +true+ if this object is the first in the list.
143         def first?
144           return false unless in_list?
145           self.send(position_column) == 1
146         end
147
148         # Return +true+ if this object is the last in the list.
149         def last?
150           return false unless in_list?
151           self.send(position_column) == bottom_position_in_list
152         end
153
154         # Return the next higher item in the list.
155         def higher_item
156           return nil unless in_list?
157           acts_as_list_class.find(:first, :conditions =>
158             "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
159           )
160         end
161
162         # Return the next lower item in the list.
163         def lower_item
164           return nil unless in_list?
165           acts_as_list_class.find(:first, :conditions =>
166             "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
167           )
168         end
169
170         # Test if this record is in a list
171         def in_list?
172           !send(position_column).nil?
173         end
174
175         private
176           def add_to_list_top
177             increment_positions_on_all_items
178           end
179
180           def add_to_list_bottom
181             self[position_column] = bottom_position_in_list.to_i + 1
182           end
183
184           # Overwrite this method to define the scope of the list changes
185           def scope_condition() "1" end
186
187           # Returns the bottom position number in the list.
188           #   bottom_position_in_list    # => 2
189           def bottom_position_in_list(except = nil)
190             item = bottom_item(except)
191             item ? item.send(position_column) : 0
192           end
193
194           # Returns the bottom item
195           def bottom_item(except = nil)
196             conditions = scope_condition
197             conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
198             acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
199           end
200
201           # Forces item to assume the bottom position in the list.
202           def assume_bottom_position
203             update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
204           end
205
206           # Forces item to assume the top position in the list.
207           def assume_top_position
208             update_attribute(position_column, 1)
209           end
210
211           # This has the effect of moving all the higher items up one.
212           def decrement_positions_on_higher_items(position)
213             acts_as_list_class.update_all(
214               "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
215             )
216           end
217
218           # This has the effect of moving all the lower items up one.
219           def decrement_positions_on_lower_items
220             return unless in_list?
221             acts_as_list_class.update_all(
222               "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
223             )
224           end
225
226           # This has the effect of moving all the higher items down one.
227           def increment_positions_on_higher_items
228             return unless in_list?
229             acts_as_list_class.update_all(
230               "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
231             )
232           end
233
234           # This has the effect of moving all the lower items down one.
235           def increment_positions_on_lower_items(position)
236             acts_as_list_class.update_all(
237               "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
238            )
239           end
240
241           # Increments position (<tt>position_column</tt>) of all items in the list.
242           def increment_positions_on_all_items
243             acts_as_list_class.update_all(
244               "#{position_column} = (#{position_column} + 1)",  "#{scope_condition}"
245             )
246           end
247
248           def insert_at_position(position)
249             remove_from_list
250             increment_positions_on_lower_items(position)
251             self.update_attribute(position_column, position)
252           end
253       end 
254     end
255   end
256 end

Benjamin Mako Hill || Want to submit a patch?