--- /dev/null
+module ActiveRecord
+ module Acts #:nodoc:
+ module List #:nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # 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.
+ #
+ # Todo list example:
+ #
+ # class TodoList < ActiveRecord::Base
+ # has_many :todo_items, :order => "position"
+ # end
+ #
+ # class TodoItem < ActiveRecord::Base
+ # belongs_to :todo_list
+ # acts_as_list :scope => :todo_list
+ # end
+ #
+ # todo_list.first.move_to_bottom
+ # todo_list.last.move_higher
+ module ClassMethods
+ # Configuration options are:
+ #
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
+ def acts_as_list(options = {})
+ configuration = { :column => "position", :scope => "1 = 1" }
+ configuration.update(options) if options.is_a?(Hash)
+
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
+
+ if configuration[:scope].is_a?(Symbol)
+ scope_condition_method = %(
+ def scope_condition
+ if #{configuration[:scope].to_s}.nil?
+ "#{configuration[:scope].to_s} IS NULL"
+ else
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
+ end
+ end
+ )
+ else
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
+ end
+
+ class_eval <<-EOV
+ include ActiveRecord::Acts::List::InstanceMethods
+
+ def acts_as_list_class
+ ::#{self.name}
+ end
+
+ def position_column
+ '#{configuration[:column]}'
+ end
+
+ #{scope_condition_method}
+
+ before_destroy :remove_from_list
+ before_create :add_to_list_bottom
+ EOV
+ end
+ end
+
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
+ # the first in the list of all chapters.
+ module InstanceMethods
+ # Insert the item at the given position (defaults to the top position of 1).
+ def insert_at(position = 1)
+ insert_at_position(position)
+ end
+
+ # Swap positions with the next lower item, if one exists.
+ def move_lower
+ return unless lower_item
+
+ acts_as_list_class.transaction do
+ lower_item.decrement_position
+ increment_position
+ end
+ end
+
+ # Swap positions with the next higher item, if one exists.
+ def move_higher
+ return unless higher_item
+
+ acts_as_list_class.transaction do
+ higher_item.increment_position
+ decrement_position
+ end
+ end
+
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
+ # position adjusted accordingly.
+ def move_to_bottom
+ return unless in_list?
+ acts_as_list_class.transaction do
+ decrement_positions_on_lower_items
+ assume_bottom_position
+ end
+ end
+
+ # Move to the top of the list. If the item is already in the list, the items above it have their
+ # position adjusted accordingly.
+ def move_to_top
+ return unless in_list?
+ acts_as_list_class.transaction do
+ increment_positions_on_higher_items
+ assume_top_position
+ end
+ end
+
+ # Removes the item from the list.
+ def remove_from_list
+ if in_list?
+ decrement_positions_on_lower_items
+ update_attribute position_column, nil
+ end
+ end
+
+ # Increase the position of this item without adjusting the rest of the list.
+ def increment_position
+ return unless in_list?
+ update_attribute position_column, self.send(position_column).to_i + 1
+ end
+
+ # Decrease the position of this item without adjusting the rest of the list.
+ def decrement_position
+ return unless in_list?
+ update_attribute position_column, self.send(position_column).to_i - 1
+ end
+
+ # Return +true+ if this object is the first in the list.
+ def first?
+ return false unless in_list?
+ self.send(position_column) == 1
+ end
+
+ # Return +true+ if this object is the last in the list.
+ def last?
+ return false unless in_list?
+ self.send(position_column) == bottom_position_in_list
+ end
+
+ # Return the next higher item in the list.
+ def higher_item
+ return nil unless in_list?
+ acts_as_list_class.find(:first, :conditions =>
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
+ )
+ end
+
+ # Return the next lower item in the list.
+ def lower_item
+ return nil unless in_list?
+ acts_as_list_class.find(:first, :conditions =>
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
+ )
+ end
+
+ # Test if this record is in a list
+ def in_list?
+ !send(position_column).nil?
+ end
+
+ private
+ def add_to_list_top
+ increment_positions_on_all_items
+ end
+
+ def add_to_list_bottom
+ self[position_column] = bottom_position_in_list.to_i + 1
+ end
+
+ # Overwrite this method to define the scope of the list changes
+ def scope_condition() "1" end
+
+ # Returns the bottom position number in the list.
+ # bottom_position_in_list # => 2
+ def bottom_position_in_list(except = nil)
+ item = bottom_item(except)
+ item ? item.send(position_column) : 0
+ end
+
+ # Returns the bottom item
+ def bottom_item(except = nil)
+ conditions = scope_condition
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
+ acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
+ end
+
+ # Forces item to assume the bottom position in the list.
+ def assume_bottom_position
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
+ end
+
+ # Forces item to assume the top position in the list.
+ def assume_top_position
+ update_attribute(position_column, 1)
+ end
+
+ # This has the effect of moving all the higher items up one.
+ def decrement_positions_on_higher_items(position)
+ acts_as_list_class.update_all(
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
+ )
+ end
+
+ # This has the effect of moving all the lower items up one.
+ def decrement_positions_on_lower_items
+ return unless in_list?
+ acts_as_list_class.update_all(
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
+ )
+ end
+
+ # This has the effect of moving all the higher items down one.
+ def increment_positions_on_higher_items
+ return unless in_list?
+ acts_as_list_class.update_all(
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
+ )
+ end
+
+ # This has the effect of moving all the lower items down one.
+ def increment_positions_on_lower_items(position)
+ acts_as_list_class.update_all(
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
+ )
+ end
+
+ # Increments position (<tt>position_column</tt>) of all items in the list.
+ def increment_positions_on_all_items
+ acts_as_list_class.update_all(
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
+ )
+ end
+
+ def insert_at_position(position)
+ remove_from_list
+ increment_positions_on_lower_items(position)
+ self.update_attribute(position_column, position)
+ end
+ end
+ end
+ end
+end