module ActiveRecord::Acts::List::InstanceMethods

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.

Public Instance Methods

decrement_position() click to toggle source

Decrease the position of this item without adjusting the rest of the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 138
def decrement_position
  return unless in_list?
  set_list_position(self.send(position_column).to_i - 1)
end
default_position() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 198
def default_position
  acts_as_list_class.columns_hash[position_column.to_s].default
end
default_position?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 202
def default_position?
  default_position && default_position.to_i == send(position_column)
end
first?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 143
def first?
  return false unless in_list?
  !higher_items(1).exists?
end
higher_item() click to toggle source

Return the next higher item in the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 154
def higher_item
  return nil unless in_list?
  higher_items(1).first
end
higher_items(limit=nil) click to toggle source

Return the next n higher items in the list selects all higher items by default

# File lib/acts_as_list/active_record/acts/list.rb, line 161
def higher_items(limit=nil)
  limit ||= acts_as_list_list.count
  position_value = send(position_column)
  acts_as_list_list.
    where("#{quoted_position_column_with_table_name} <= ?", position_value).
    where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
    reorder("#{quoted_position_column_with_table_name} DESC").
    limit(limit)
end
in_list?() click to toggle source

Test if this record is in a list

# File lib/acts_as_list/active_record/acts/list.rb, line 190
def in_list?
  !not_in_list?
end
increment_position() click to toggle source

Increase the position of this item without adjusting the rest of the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 132
def increment_position
  return unless in_list?
  set_list_position(self.send(position_column).to_i + 1)
end
insert_at(position = acts_as_list_top) click to toggle source

Insert the item at the given position (defaults to the top position of 1).

# File lib/acts_as_list/active_record/acts/list.rb, line 64
def insert_at(position = acts_as_list_top)
  insert_at_position(position)
end
last?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 148
def last?
  return false unless in_list?
  !lower_items(1).exists?
end
lower_item() click to toggle source

Return the next lower item in the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 172
def lower_item
  return nil unless in_list?
  lower_items(1).first
end
lower_items(limit=nil) click to toggle source

Return the next n lower items in the list selects all lower items by default

# File lib/acts_as_list/active_record/acts/list.rb, line 179
def lower_items(limit=nil)
  limit ||= acts_as_list_list.count
  position_value = send(position_column)
  acts_as_list_list.
    where("#{quoted_position_column_with_table_name} >= ?", position_value).
    where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
    reorder("#{quoted_position_column_with_table_name} ASC").
    limit(limit)
end
move_higher() click to toggle source

Swap positions with the next higher item, if one exists.

# File lib/acts_as_list/active_record/acts/list.rb, line 83
def move_higher
  return unless higher_item

  acts_as_list_class.transaction do
    if higher_item.send(position_column) != self.send(position_column)
      swap_positions(higher_item, self)
    else
      higher_item.increment_position
      decrement_position
    end
  end
end
move_lower() click to toggle source

Swap positions with the next lower item, if one exists.

# File lib/acts_as_list/active_record/acts/list.rb, line 69
def move_lower
  return unless lower_item

  acts_as_list_class.transaction do
    if lower_item.send(position_column) != self.send(position_column)
      swap_positions(lower_item, self)
    else
      lower_item.decrement_position
      increment_position
    end
  end
end
move_to_bottom() click to toggle source

Move to the bottom of the list. If the item is already in the list, the items below it have their position adjusted accordingly.

# File lib/acts_as_list/active_record/acts/list.rb, line 98
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_top() click to toggle source

Move to the top of the list. If the item is already in the list, the items above it have their position adjusted accordingly.

# File lib/acts_as_list/active_record/acts/list.rb, line 108
def move_to_top
  return unless in_list?
  acts_as_list_class.transaction do
    increment_positions_on_higher_items
    assume_top_position
  end
end
move_within_scope(scope_id) click to toggle source

Move the item within scope. If a position within the new scope isn't supplied, the item will be appended to the end of the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 126
def move_within_scope(scope_id)
  send("#{scope_name}=", scope_id)
  save!
end
not_in_list?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 194
def not_in_list?
  send(position_column).nil?
end
remove_from_list() click to toggle source

Removes the item from the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 117
def remove_from_list
  if in_list?
    decrement_positions_on_lower_items
    set_list_position(nil)
  end
end
set_list_position(new_position) click to toggle source

Sets the new position and saves it

# File lib/acts_as_list/active_record/acts/list.rb, line 207
def set_list_position(new_position)
  write_attribute position_column, new_position
  save(validate: false)
end

Private Instance Methods

acts_as_list_list() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 221
def acts_as_list_list
  if ActiveRecord::VERSION::MAJOR < 4
    acts_as_list_class.unscoped do
      acts_as_list_class.where(scope_condition)
    end
  else
    acts_as_list_class.unscope(:where).where(scope_condition)
  end
end
add_to_list_bottom() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 249
def add_to_list_bottom
  if not_in_list? || internal_scope_changed? && !position_changed || default_position?
    self[position_column] = bottom_position_in_list.to_i + 1
  else
    increment_positions_on_lower_items(self[position_column], id)
  end

  # Make sure we know that we've processed this scope change already
  @scope_changed = false

  # Don't halt the callback chain
  true
end
add_to_list_top() click to toggle source

Poorly named methods. They will insert the item at the desired position if the position has been set manually using position=, not necessarily the top or bottom of the list:

# File lib/acts_as_list/active_record/acts/list.rb, line 234
def add_to_list_top
  if not_in_list? || internal_scope_changed? && !position_changed || default_position?
    increment_positions_on_all_items
    self[position_column] = acts_as_list_top
  else
    increment_positions_on_lower_items(self[position_column], id)
  end

  # Make sure we know that we've processed this scope change already
  @scope_changed = false

  # Don't halt the callback chain
  true
end
assume_bottom_position() click to toggle source

Forces item to assume the bottom position in the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 285
def assume_bottom_position
  set_list_position(bottom_position_in_list(self).to_i + 1)
end
assume_top_position() click to toggle source

Forces item to assume the top position in the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 290
def assume_top_position
  set_list_position(acts_as_list_top)
end
bottom_item(except = nil) click to toggle source

Returns the bottom item

# File lib/acts_as_list/active_record/acts/list.rb, line 274
def bottom_item(except = nil)
  scope = acts_as_list_list

  if except
    scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", except.id)
  end

  scope.in_list.reorder("#{quoted_position_column_with_table_name} DESC").first
end
bottom_position_in_list(except = nil) click to toggle source

Returns the bottom position number in the list.

bottom_position_in_list    # => 2
# File lib/acts_as_list/active_record/acts/list.rb, line 268
def bottom_position_in_list(except = nil)
  item = bottom_item(except)
  item ? item.send(position_column) : acts_as_list_top - 1
end
check_scope() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 429
def check_scope
  if internal_scope_changed?
    cached_changes = changes

    cached_changes.each { |attribute, values| self[attribute] = values[0] }
    send('decrement_positions_on_lower_items') if lower_item
    cached_changes.each { |attribute, values| self[attribute] = values[1] }

    send("add_to_list_#{add_new_at}") if add_new_at.present?
  end
end
check_top_position() click to toggle source

This check is skipped if the position is currently the default position from the table as modifying the default position on creation is handled elsewhere

# File lib/acts_as_list/active_record/acts/list.rb, line 443
def check_top_position
  if send(position_column) && !default_position? && send(position_column) < acts_as_list_top
    self[position_column] = acts_as_list_top
  end
end
clear_scope_changed() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 425
def clear_scope_changed
  remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
end
decrement_positions_on_higher_items(position) click to toggle source

This has the effect of moving all the higher items up one.

# File lib/acts_as_list/active_record/acts/list.rb, line 312
def decrement_positions_on_higher_items(position)
  acts_as_list_list.where("#{quoted_position_column_with_table_name} <= ?", position).decrement_all
end
decrement_positions_on_lower_items(position=nil) click to toggle source

This has the effect of moving all the lower items up one.

# File lib/acts_as_list/active_record/acts/list.rb, line 317
def decrement_positions_on_lower_items(position=nil)
  return unless in_list?
  position ||= send(position_column).to_i
  acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
end
increment_positions_on_all_items() click to toggle source

Increments position (position_column) of all items in the list.

# File lib/acts_as_list/active_record/acts/list.rb, line 324
def increment_positions_on_all_items
  acts_as_list_list.increment_all
end
increment_positions_on_higher_items() click to toggle source

This has the effect of moving all the higher items down one.

# File lib/acts_as_list/active_record/acts/list.rb, line 295
def increment_positions_on_higher_items
  return unless in_list?
  acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", send(position_column).to_i).increment_all
end
increment_positions_on_lower_items(position, avoid_id = nil) click to toggle source

This has the effect of moving all the lower items down one.

# File lib/acts_as_list/active_record/acts/list.rb, line 301
def increment_positions_on_lower_items(position, avoid_id = nil)
  scope = acts_as_list_list

  if avoid_id
    scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
  end

  scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
end
insert_at_position(position) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 380
def insert_at_position(position)
  return set_list_position(position) if new_record?
  with_lock do
    if in_list?
      old_position = send(position_column).to_i
      return if position == old_position
      # temporary move after bottom with gap, avoiding duplicate values
      # gap is required to leave room for position increments
      # positive number will be valid with unique not null check (>= 0) db constraint
      temporary_position = bottom_position_in_list + 2
      set_list_position(temporary_position)
      shuffle_positions_on_intermediate_items(old_position, position, id)
    else
      increment_positions_on_lower_items(position)
    end
    set_list_position(position)
  end
end
internal_scope_changed?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 419
def internal_scope_changed?
  return @scope_changed if defined?(@scope_changed)

  @scope_changed = scope_changed?
end
position_before_save() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 409
def position_before_save
  if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1 ||
    ActiveRecord::VERSION::MAJOR > 5

    send("#{position_column}_before_last_save")
  else
    send("#{position_column}_was")
  end
end
quoted_position_column() click to toggle source

When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)

# File lib/acts_as_list/active_record/acts/list.rb, line 450
def quoted_position_column
  @_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
end
quoted_position_column_with_table_name() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 459
def quoted_position_column_with_table_name
  @_quoted_position_column_with_table_name ||= "#{quoted_table_name}.#{quoted_position_column}"
end
quoted_table_name() click to toggle source

Used in order clauses

# File lib/acts_as_list/active_record/acts/list.rb, line 455
def quoted_table_name
  @_quoted_table_name ||= acts_as_list_class.quoted_table_name
end
scope_condition() click to toggle source

Overwrite this method to define the scope of the list changes

# File lib/acts_as_list/active_record/acts/list.rb, line 264
def scope_condition() {} end
shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil) click to toggle source

Reorders intermediate items to support moving an item from old_position to new_position. unique constraint prevents regular increment_all and forces to do increments one by one stackoverflow.com/questions/7703196/sqlite-increment-unique-integer-field both SQLite and PostgreSQL (and most probably MySQL too) has same issue that's why sequential_updates? check alters implementation behavior

# File lib/acts_as_list/active_record/acts/list.rb, line 333
def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
  return if old_position == new_position
  scope = acts_as_list_list

  if avoid_id
    scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
  end

  if old_position < new_position
    # Decrement position of intermediate items
    #
    # e.g., if moving an item from 2 to 5,
    # move [3, 4, 5] to [2, 3, 4]
    items = scope.where(
      "#{quoted_position_column_with_table_name} > ?", old_position
    ).where(
      "#{quoted_position_column_with_table_name} <= ?", new_position
    )

    if sequential_updates?
      items.reorder("#{quoted_position_column_with_table_name} ASC").each do |item|
        item.decrement!(position_column)
      end
    else
      items.decrement_all
    end
  else
    # Increment position of intermediate items
    #
    # e.g., if moving an item from 5 to 2,
    # move [2, 3, 4] to [3, 4, 5]
    items = scope.where(
      "#{quoted_position_column_with_table_name} >= ?", new_position
    ).where(
      "#{quoted_position_column_with_table_name} < ?", old_position
    )

    if sequential_updates?
      items.reorder("#{quoted_position_column_with_table_name} DESC").each do |item|
        item.increment!(position_column)
      end
    else
      items.increment_all
    end
  end
end
swap_positions(item1, item2) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 214
def swap_positions(item1, item2)
  item1_position = item1.send(position_column)

  item1.set_list_position(item2.send(position_column))
  item2.set_list_position(item1_position)
end
update_positions() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 399
def update_positions
  old_position = position_before_save || bottom_position_in_list + 1
  new_position = send(position_column).to_i

  return unless acts_as_list_list.where(
    "#{quoted_position_column_with_table_name} = #{new_position}"
  ).count > 1
  shuffle_positions_on_intermediate_items old_position, new_position, id
end