module ActiveRecord::Acts::List::InstanceMethods
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 146 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 206 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 210 def default_position? default_position && default_position.to_i == send(position_column) end
# File lib/acts_as_list/active_record/acts/list.rb, line 151 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 162 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 169 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
Test if this record is in a list
# File lib/acts_as_list/active_record/acts/list.rb, line 198 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 140 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 68 def insert_at(position = acts_as_list_top) insert_at_position(position) end
# File lib/acts_as_list/active_record/acts/list.rb, line 72 def insert_at!(position = acts_as_list_top) insert_at_position(position, true) end
# File lib/acts_as_list/active_record/acts/list.rb, line 156 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 180 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 187 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
Swap positions with the next higher item, if one exists.
# File lib/acts_as_list/active_record/acts/list.rb, line 91 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 77 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 106 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 116 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 134 def move_within_scope(scope_id) send("#{scope_name}=", scope_id) save! end
# File lib/acts_as_list/active_record/acts/list.rb, line 202 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 125 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 215 def set_list_position(new_position, raise_exception_if_save_fails=false) write_attribute position_column, new_position raise_exception_if_save_fails ? save! : save end
Private Instance Methods
# File lib/acts_as_list/active_record/acts/list.rb, line 229 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(:select, :where).where(scope_condition) end end
# File lib/acts_as_list/active_record/acts/list.rb, line 496 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
# File lib/acts_as_list/active_record/acts/list.rb, line 257 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
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 242 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
Forces item to assume the bottom position in the list.
# File lib/acts_as_list/active_record/acts/list.rb, line 299 def assume_bottom_position set_list_position(bottom_position_in_list(self).to_i + 1) end
# File lib/acts_as_list/active_record/acts/list.rb, line 271 def assume_default_position? not_in_list? || persisted? && internal_scope_changed? && !position_changed || default_position? end
Forces item to assume the top position in the list.
# File lib/acts_as_list/active_record/acts/list.rb, line 304 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 288 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
Returns the bottom position number in the list.
bottom_position_in_list # => 2
# File lib/acts_as_list/active_record/acts/list.rb, line 282 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 462 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
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 476 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 458 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 326 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 331 def decrement_positions_on_lower_items(position=nil) return unless in_list? position ||= send(position_column).to_i if sequential_updates? acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).reorder(acts_as_list_order_argument(:asc)).each do |item| item.decrement!(position_column) end else acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all end end
Increments position (position_column
) of all items in the list.
# File lib/acts_as_list/active_record/acts/list.rb, line 345 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 309 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 315 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 401 def insert_at_position(position, raise_exception_if_save_fails=false) return set_list_position(position, raise_exception_if_save_fails) 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, 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
# File lib/acts_as_list/active_record/acts/list.rb, line 452 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 442 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
# File lib/acts_as_list/active_record/acts/list.rb, line 432 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
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 483 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 492 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 488 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 278 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 354 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
# File lib/acts_as_list/active_record/acts/list.rb, line 222 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 420 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