X-Git-Url: https://projects.mako.cc/source/selectricity-live/blobdiff_plain/05ebed925ae2b5e7bf2a599536ba7d7ac15ffbf7..ad088c1324d08a65f6f5336bedf7a88a8a8950e7:/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb diff --git a/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb b/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb new file mode 100644 index 0000000..00d8692 --- /dev/null +++ b/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb @@ -0,0 +1,256 @@ +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 _id + # (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: 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 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 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 + # 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 (position_column) 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