module ActiveRecord module Acts #:nodoc: module List #:nodoc: def self.append_features(base) super base.extend(ClassMethods) end # This act provides the capabilities for sorting and reordering a number of objects in 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 "_id" # (if that hasn't been already) 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: acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' 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 position_column '#{configuration[:column]}' end #{scope_condition_method} after_destroy :remove_from_list before_create :add_to_list_bottom EOV end end # All the methods available to a record that has had acts_as_list specified. Each method works # by assuming the object to be the item in the list, so chapter.move_lower would move that chapter # lower in the list of all chapters. Likewise, chapter.first? would return true if that chapter is # the first in the list of all chapters. module InstanceMethods def insert_at(position = 1) position == 1 ? add_to_list_top : insert_at_position(position) end def move_lower return unless lower_item self.class.transaction do lower_item.decrement_position increment_position end end def move_higher return unless higher_item self.class.transaction do higher_item.increment_position decrement_position end end def move_to_bottom return unless in_list? self.class.transaction do decrement_positions_on_lower_items assume_bottom_position end end def move_to_top return unless in_list? self.class.transaction do increment_positions_on_higher_items assume_top_position end end def remove_from_list decrement_positions_on_lower_items if in_list? end def increment_position return unless in_list? update_attribute position_column, self.send(position_column).to_i + 1 end def decrement_position return unless in_list? update_attribute position_column, self.send(position_column).to_i - 1 end def first? return false unless in_list? self.send(position_column) == 1 end def last? return false unless in_list? self.send(position_column) == bottom_position_in_list end def higher_item return nil unless in_list? self.class.find(:first, :conditions => "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}" ) end def lower_item return nil unless in_list? self.class.find(:first, :conditions => "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}" ) end 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 def bottom_position_in_list item = bottom_item item ? item.send(position_column) : 0 end def bottom_item self.class.find(:first, :conditions => scope_condition, :order => "#{position_column} DESC") end def assume_bottom_position update_attribute(position_column, bottom_position_in_list.to_i + 1) end 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) self.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? self.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? self.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) self.class.update_all( "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}" ) end def increment_positions_on_all_items self.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