module ActiveRecord::Acts::List::InstanceMethods

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 142
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 202
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 206
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 147
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 158
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 165
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(acts_as_list_order_argument(: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 194
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 136
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 68
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 152
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 176
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 183
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(acts_as_list_order_argument(: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 87
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 73
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 102
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 112
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 130
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 198
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 121
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 211
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 225
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
acts_as_list_order_argument(direction = :asc) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 485
def acts_as_list_order_argument(direction = :asc)
  if ActiveRecord::VERSION::MAJOR >= 4
    { position_column => direction }
  else
    "#{quoted_position_column_with_table_name} #{direction.to_s.upcase}"
  end
end
add_to_list_bottom() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 253
def add_to_list_bottom
  if assume_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 238
def add_to_list_top
  if assume_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 295
def assume_bottom_position
  set_list_position(bottom_position_in_list(self).to_i + 1)
end
assume_default_position?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 267
def assume_default_position?
  not_in_list? ||
  persisted? && internal_scope_changed? && !position_changed ||
  default_position?
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 300
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 284
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(acts_as_list_order_argument(: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 278
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 451
def check_scope
  if internal_scope_changed?
    cached_changes = changes

    cached_changes.each { |attribute, values| send("#{attribute}=", values[0]) }
    send('decrement_positions_on_lower_items') if lower_item
    cached_changes.each { |attribute, values| send("#{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 465
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 447
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 322
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 327
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 334
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 305
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 311
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 390
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 441
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 431
def position_before_save
  if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1 ||
      ActiveRecord::VERSION::MAJOR > 5

    attribute_before_last_save position_column
  else
    send "#{position_column}_was"
  end
end
position_before_save_changed?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 421
def position_before_save_changed?
  if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1 ||
      ActiveRecord::VERSION::MAJOR > 5

    saved_change_to_attribute? position_column
  else
    send "#{position_column}_changed?"
  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 472
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 481
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 477
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 274
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 343
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(acts_as_list_order_argument(: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(acts_as_list_order_argument(: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 218
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 409
def update_positions
  return unless position_before_save_changed?

  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