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