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
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
# 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
# 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
# File lib/acts_as_list/active_record/acts/list.rb, line 143 def first? return false unless in_list? !higher_items(1).exists? end
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
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
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
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 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
# File lib/acts_as_list/active_record/acts/list.rb, line 148 def last? return false unless in_list? !lower_items(1).exists? end
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
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
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
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 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 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 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
# File lib/acts_as_list/active_record/acts/list.rb, line 194 def not_in_list? send(position_column).nil? end
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
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
# 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
# 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
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
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
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
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
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
# 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
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
# 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
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
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
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
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
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
# 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
# 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
# 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
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
# 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
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
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
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
# 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
# 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