beast rev 2066
[beast-modified.git] / vendor / plugins / acts_as_list / lib / active_record / acts / list.rb
blob52a32a1789f4529930628acd26f6c3270f7024a4
1 module ActiveRecord
2   module Acts #:nodoc:
3     module List #:nodoc:
4       def self.included(base)
5         base.extend(ClassMethods)
6       end
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)
37           configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
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
53           class_eval <<-EOV
54             include ActiveRecord::Acts::List::InstanceMethods
56             def acts_as_list_class
57               ::#{self.name}
58             end
60             def position_column
61               '#{configuration[:column]}'
62             end
64             #{scope_condition_method}
66             after_destroy  :remove_from_list
67             before_create  :add_to_list_bottom
68           EOV
69         end
70       end
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
82         # Swap positions with the next lower item, if one exists.
83         def move_lower
84           return unless lower_item
86           acts_as_list_class.transaction do
87             lower_item.decrement_position
88             increment_position
89           end
90         end
92         # Swap positions with the next higher item, if one exists.
93         def move_higher
94           return unless higher_item
96           acts_as_list_class.transaction do
97             higher_item.increment_position
98             decrement_position
99           end
100         end
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
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
122         # Removes the item from the list.
123         def remove_from_list
124           decrement_positions_on_lower_items if in_list?
125         end
127         # Increase the position of this item without adjusting the rest of the list.
128         def increment_position
129           return unless in_list?
130           update_attribute position_column, self.send(position_column).to_i + 1
131         end
133         # Decrease the position of this item without adjusting the rest of the list.
134         def decrement_position
135           return unless in_list?
136           update_attribute position_column, self.send(position_column).to_i - 1
137         end
139         # Return +true+ if this object is the first in the list.
140         def first?
141           return false unless in_list?
142           self.send(position_column) == 1
143         end
145         # Return +true+ if this object is the last in the list.
146         def last?
147           return false unless in_list?
148           self.send(position_column) == bottom_position_in_list
149         end
151         # Return the next higher item in the list.
152         def higher_item
153           return nil unless in_list?
154           acts_as_list_class.find(:first, :conditions =>
155             "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
156           )
157         end
159         # Return the next lower item in the list.
160         def lower_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
167         # Test if this record is in a list
168         def in_list?
169           !send(position_column).nil?
170         end
172         private
173           def add_to_list_top
174             increment_positions_on_all_items
175           end
177           def add_to_list_bottom
178             self[position_column] = bottom_position_in_list.to_i + 1
179           end
181           # Overwrite this method to define the scope of the list changes
182           def scope_condition() "1" end
184           # Returns the bottom position number in the list.
185           #   bottom_position_in_list    # => 2
186           def bottom_position_in_list(except = nil)
187             item = bottom_item(except)
188             item ? item.send(position_column) : 0
189           end
191           # Returns the bottom item
192           def bottom_item(except = nil)
193             conditions = scope_condition
194             conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
195             acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
196           end
198           # Forces item to assume the bottom position in the list.
199           def assume_bottom_position
200             update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
201           end
203           # Forces item to assume the top position in the list.
204           def assume_top_position
205             update_attribute(position_column, 1)
206           end
208           # This has the effect of moving all the higher items up one.
209           def decrement_positions_on_higher_items(position)
210             acts_as_list_class.update_all(
211               "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
212             )
213           end
215           # This has the effect of moving all the lower items up one.
216           def decrement_positions_on_lower_items
217             return unless in_list?
218             acts_as_list_class.update_all(
219               "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
220             )
221           end
223           # This has the effect of moving all the higher items down one.
224           def increment_positions_on_higher_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
231           # This has the effect of moving all the lower items down one.
232           def increment_positions_on_lower_items(position)
233             acts_as_list_class.update_all(
234               "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
235            )
236           end
238           # Increments position (<tt>position_column</tt>) of all items in the list.
239           def increment_positions_on_all_items
240             acts_as_list_class.update_all(
241               "#{position_column} = (#{position_column} + 1)",  "#{scope_condition}"
242             )
243           end
245           def insert_at_position(position)
246             remove_from_list
247             increment_positions_on_lower_items(position)
248             self.update_attribute(position_column, position)
249           end
250       end 
251     end
252   end