module ActiveRecord::Acts::List::InstanceMethods

Public Instance Methods

current_position() click to toggle source

Get the current position of the item in the list

# File lib/acts_as_list/active_record/acts/list.rb, line 69
def current_position
  position = send(position_column)
  position ? position.to_i : nil
end
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 147
def decrement_position
  return unless in_list?
  set_list_position(current_position - 1)
end
default_position() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 205
def default_position
  acts_as_list_class.column_defaults[position_column.to_s]
end
default_position?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 209
def default_position?
  default_position && default_position == current_position
end
first?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 152
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 163
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 170
def higher_items(limit=nil)
  limit ||= acts_as_list_list.count
  acts_as_list_list.
    where("#{quoted_position_column_with_table_name} <= ?", current_position).
    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 197
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 141
def increment_position
  return unless in_list?
  set_list_position(current_position + 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 75
def insert_at(position = acts_as_list_top)
  insert_at_position(position)
end
insert_at!(position = acts_as_list_top) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 79
def insert_at!(position = acts_as_list_top)
  insert_at_position(position, true)
end
last?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 157
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 180
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 187
def lower_items(limit=nil)
  limit ||= acts_as_list_list.count
  acts_as_list_list.
    where("#{quoted_position_column_with_table_name} >= ?", current_position).
    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 98
def move_higher
  return unless higher_item

  acts_as_list_class.transaction do
    if higher_item.current_position != current_position
      swap_positions_with(higher_item)
    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 84
def move_lower
  return unless lower_item

  acts_as_list_class.transaction do
    if lower_item.current_position != current_position
      swap_positions_with(lower_item)
    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 113
def move_to_bottom
  return unless in_list?
  insert_at_position bottom_position_in_list.to_i
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 120
def move_to_top
  return unless in_list?
  insert_at_position acts_as_list_top
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 135
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 201
def not_in_list?
  current_position.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 126
def remove_from_list
  if in_list?
    decrement_positions_on_lower_items
    set_list_position(nil)
  end
end
set_list_position(new_position, raise_exception_if_save_fails=false) click to toggle source

Sets the new position and saves it

# File lib/acts_as_list/active_record/acts/list.rb, line 214
def set_list_position(new_position, raise_exception_if_save_fails=false)
  self[position_column] = new_position
  raise_exception_if_save_fails ? save! : save
end

Private Instance Methods

active_record_version_is?(version_requirement) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 478
def active_record_version_is?(version_requirement)
  requirement = Gem::Requirement.new(version_requirement)
  version = Gem.loaded_specs['activerecord'].version
  requirement.satisfied_by?(version)
end
acts_as_list_list() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 228
def acts_as_list_list
  acts_as_list_class.default_scoped.unscope(:select, :where).where(scope_condition)
end
acts_as_list_order_argument(direction = :asc) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 474
def acts_as_list_order_argument(direction = :asc)
  { position_column => direction }
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 283
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 255
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 288
def assume_top_position
  set_list_position(acts_as_list_top)
end
avoid_collision() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 232
def avoid_collision
  case add_new_at
  when :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
  when :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
  else
    increment_positions_on_lower_items(self[position_column], id) if position_changed
  end

  @scope_changed = false # Make sure we know that we've processed this scope change already
  return true # Don't halt the callback chain
end
bottom_item(except = nil) click to toggle source

Returns the bottom item

# File lib/acts_as_list/active_record/acts/list.rb, line 272
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 266
def bottom_position_in_list(except = nil)
  item = bottom_item(except)
  item ? item.current_position : acts_as_list_top - 1
end
check_scope() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 440
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]) }

    avoid_collision
  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 454
def check_top_position
  if current_position && !default_position? && current_position < 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 436
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 314
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=current_position) 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 319
def decrement_positions_on_lower_items(position=current_position)
  return unless in_list?

  if sequential_updates?
    acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
  else
    acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
  end
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 330
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 293
def increment_positions_on_higher_items
  return unless in_list?
  acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", current_position).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 299
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

  if sequential_updates?
    scope.where("#{quoted_position_column_with_table_name} >= ?", position).reorder(acts_as_list_order_argument(:desc)).increment_sequentially
  else
    scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
  end
end
insert_at_position(position, raise_exception_if_save_fails=false) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 382
def insert_at_position(position, raise_exception_if_save_fails=false)
  raise ArgumentError.new("position cannot be lower than top") if position < acts_as_list_top
  return set_list_position(position, raise_exception_if_save_fails) if new_record?
  with_lock do
    if in_list?
      old_position = current_position
      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, raise_exception_if_save_fails)
      shuffle_positions_on_intermediate_items(old_position, position, id)
    else
      increment_positions_on_lower_items(position)
    end
    set_list_position(position, raise_exception_if_save_fails)
  end
end
internal_scope_changed?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 430
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 422
def position_before_save
  if active_record_version_is?('>= 5.1')
    attribute_before_last_save position_column
  else
    attribute_was position_column
  end
end
position_before_save_changed?() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 414
def position_before_save_changed?
  if active_record_version_is?('>= 5.1')
    saved_change_to_attribute? position_column
  else
    attribute_changed? position_column
  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 461
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 470
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 466
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 262
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 339
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)).decrement_sequentially
    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)).increment_sequentially
    else
      items.increment_all
    end
  end
end
swap_positions_with(item) click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 221
def swap_positions_with(item)
  item_position = item.current_position

  item.set_list_position(current_position)
  set_list_position(item_position)
end
update_positions() click to toggle source
# File lib/acts_as_list/active_record/acts/list.rb, line 402
def update_positions
  return unless position_before_save_changed?

  old_position = position_before_save || bottom_position_in_list + 1

  return unless current_position && acts_as_list_list.where(
    "#{quoted_position_column_with_table_name} = #{current_position}"
  ).count > 1

  shuffle_positions_on_intermediate_items old_position, current_position, id
end