module Sortability::ActiveRecord::Base::ClassMethods

Public Instance Methods

sortable_belongs_to(container, scope_or_options = nil, options_with_scope = {}, &extension) click to toggle source

Defines a sortable belongs_to relation on the child records

# File lib/sortability/active_record/base.rb, line 140
def sortable_belongs_to(container, scope_or_options = nil,
                        options_with_scope = {}, &extension)
  scope, options = extract_association_params(scope_or_options, options_with_scope)
  on = options[:on] || :sort_position

  class_exec do
    belongs_to container, scope, options.except(:on, :scope), &extension

    reflection = reflect_on_association(container)
    options[:scope] ||= reflection.polymorphic? ? \
                          [reflection.foreign_type,
                           reflection.foreign_key] : \
                          reflection.foreign_key
    options[:inverse_of] ||= reflection.inverse_of.try(:name)

    validates on, presence: true,
                  numericality: { only_integer: true,
                                  greater_than: 0 },
                  uniqueness: { scope: options[:scope] }
  end

  options[:container] = container
  sortable_methods(options)
end
sortable_class(options = {}) click to toggle source

Defines a sortable class without a container

# File lib/sortability/active_record/base.rb, line 166
def sortable_class(options = {})
  on = options[:on] || :sort_position
  scope = options[:scope]

  class_exec do
    default_scope { order(on) }

    validates on, presence: true,
                  numericality: { only_integer: true,
                                  greater_than: 0 },
                  uniqueness: (scope.nil? ? true : { scope: scope })
  end

  sortable_methods(options)
end
sortable_has_many(records, scope_or_options = nil, options_with_scope = {}, &extension) click to toggle source

Defines a sortable has_many relation on the container

# File lib/sortability/active_record/base.rb, line 129
def sortable_has_many(records, scope_or_options = nil, options_with_scope = {}, &extension)
  scope, options = extract_association_params(scope_or_options, options_with_scope)
  if scope.nil?
    on = options[:on] || :sort_position
    scope = -> { order(on) }
  end

  class_exec { has_many records, scope, options.except(:on), &extension }
end
sortable_methods(options = {}) click to toggle source

Defines methods that are used to sort records Use via sortable_belongs_to or sortable_class

# File lib/sortability/active_record/base.rb, line 11
def sortable_methods(options = {})
  on = options[:on] || :sort_position
  container = options[:container]
  inverse_of = options[:inverse_of]
  scope_array = [options[:scope]].flatten.compact
  onname = on.to_s
  setter_mname = "#{onname}="
  peers_mname = "#{onname}_peers"
  before_validation_mname = "#{onname}_before_validation"
  next_by_mname = "next_by_#{onname}"
  prev_by_mname = "previous_by_#{onname}"
  compact_peers_mname = "compact_#{onname}_peers!"

  class_exec do
    before_validation before_validation_mname.to_sym

    # Returns all the sort peers for this record, including self
    define_method peers_mname do |force_scope_load = false|
      unless force_scope_load || container.nil? || inverse_of.nil?
        cont = send(container)
        return cont.send(inverse_of) unless cont.nil?
      end

      relation = self.class.unscoped
      scope_array.each do |s|
        relation = relation.where(s => send(s))
      end
      relation
    end

    # Assigns the "on" field's value if needed
    # Adds 1 to any conflicting fields
    define_method before_validation_mname do
      val = send(on)
      scope_changed = scope_array.any? { |s|
                        !changed_attributes[s].nil? }

      return unless val.nil? || scope_changed || changes[on]

      peers = send(peers_mname, scope_changed)
      if val.nil?
        # Assign the next available number to the record
        max_val = (peers.loaded? ? \
                    peers.to_a.max_by{|r| r.send(on) || 0}.try(on) : \
                    peers.maximum(on)) || 0
        send(setter_mname, max_val + 1)
      elsif peers.to_a.any? { |p| p != self && p.send(on) == val }
        # Make a gap for the record
        at = self.class.arel_table
        peers.where(at[on].gteq(val))
             .reorder(nil)
             .update_all("#{onname} = - (#{onname} + 1)")
        peers.where(at[on].lt(0))
             .reorder(nil)
             .update_all("#{onname} = - #{onname}")

        # Cause peers to load from the DB the next time they are used
        peers.reset
      end
    end

    # Gets the next record among the peers
    define_method next_by_mname do
      val = send(on)
      peers = send(peers_mname)
      peers.loaded? ? \
        peers.to_a.detect { |p| p.send(on) > val } : \
        peers.where(peers.arel_table[on].gt(val)).first
    end

    # Gets the previous record among the peers
    define_method prev_by_mname do
      val = send(on)
      peers = send(peers_mname)
      peers.loaded? ? \
        peers.to_a.reverse.detect { |p| p.send(on) < val } : \
        peers.where(peers.arel_table[on].lt(val)).last
    end

    # Renumbers the peers so that their numbers are sequential,
    # starting at 1
    define_method compact_peers_mname do
      needs_update = false
      peers = send(peers_mname)
      cases = peers.to_a.collect.with_index do |p, i|
        old_val = p.send(on)
        new_val = i + 1
        needs_update = true if old_val != new_val

        # Make sure "on" field in self is up to date
        send(setter_mname, new_val) if p == self

        "WHEN #{old_val} THEN #{- new_val}"
      end.join(' ')

      return peers unless needs_update

      mysql = \
        defined?(ActiveRecord::ConnectionAdapters::MysqlAdapter) && \
        ActiveRecord::Base.connection.instance_of?(
          ActiveRecord::ConnectionAdapters::MysqlAdapter)
      cend = mysql ? 'END CASE' : 'END'

      self.class.transaction do
        peers.reorder(nil)
             .update_all("#{onname} = CASE #{onname} #{cases} #{cend}")
        peers.reorder(nil).update_all("#{onname} = - #{onname}")
      end

      # Mark self as not dirty
      changes_applied
      # Force peers to load from the DB the next time they are used
      peers.reset
    end
  end
end

Protected Instance Methods

extract_association_params(scope_or_options, options_with_scope) click to toggle source
# File lib/sortability/active_record/base.rb, line 184
def extract_association_params(scope_or_options, options_with_scope)
  if scope_or_options.is_a?(Hash)
    [nil, scope_or_options]
  else
    [scope_or_options, options_with_scope]
  end
end