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