class Incline::DataTablesRequest

Parses the parameters sent by a request from datatables.

Public Class Methods

new(params = {}, &block) click to toggle source

Initializes the data tables request parameters.

# File lib/incline/data_tables_request.rb, line 54
def initialize(params = {}, &block)
  raise ArgumentError, 'A block is required to return the starting ActiveRecord scope.' unless block_given?

  @config                   = {}
  @config[:starting_scope]  = block

  params = params.deep_symbolize_keys

  force_regex = params[:force_regex]

  if params[:draw]
    @config[:draw]   = params[:draw].to_s.to_i
    @config[:start]  = params[:start].to_s.to_i
    @config[:length] = params[:length].to_s.to_i
    @config[:locate_id] = params[:locate_id]


    tmp = params[:search]
    if tmp && !tmp[:value].blank?
      if tmp[:regex].to_bool || force_regex
        @config[:search] =
            if tmp[:value].is_a?(::Regexp)
              tmp[:value]
            else
              begin
                Regexp.new(tmp[:value].to_s, Regexp::IGNORECASE)
              rescue RegexpError
                tmp[:value].to_s
              end
            end
      elsif tmp
        @config[:search] = tmp[:value].to_s
      end
    else
      @config[:search] = nil
    end

    tmp               = params[:columns]
    @config[:columns] = [ ]
    if tmp
      tmp = tmp.each_with_index.to_a.map{|(a,b)| [b.to_s,a]}.to_h.deep_symbolize_keys if tmp.is_a?(::Array)
      tmp.each do |id, col|
        col[:id] = id.to_s.to_i
        col[:name] = col[:data] if col[:name].blank?
        col[:searchable] = col[:searchable].to_bool
        col[:orderable] = col[:orderable].to_bool

        if col[:search] && !col[:search][:value].blank?
          if col[:search][:regex].to_bool || force_regex
            col[:search] =
                if col[:search][:value].is_a?(::Regexp)
                  col[:search][:value]
                else
                  begin
                    Regexp.new(col[:search][:value].to_s, Regexp::IGNORECASE)
                  rescue RegexpError
                    col[:search][:value].to_s
                  end
                end
          else
            col[:search] = col[:search][:value].to_s
          end
        else
          col[:search] = nil
        end

        @config[:columns] << col
      end
    end
    @config[:columns].freeze

    tmp             = params[:order]
    @config[:order] = { }
    if tmp
      tmp = tmp.each_with_index.to_a.map{|(a,b)| [b.to_s,a]}.to_h.deep_symbolize_keys if tmp.is_a?(::Array)
      tmp.each do |_, order|
        col_id = order[:column].to_i
        col = columns.find{|c| c[:id] == col_id}
        if col
          @config[:order][col[:name]] = ((order[:dir] || 'asc').downcase).to_sym
        end
      end
    end
    @config[:order].freeze
  else
    @config[:draw] = :not_provided
  end

end

Public Instance Methods

columns() click to toggle source

The columns requested.

# File lib/incline/data_tables_request.rb, line 34
def columns
  @config[:columns]
end
draw() click to toggle source

Draw counter.

# File lib/incline/data_tables_request.rb, line 8
def draw
  @config[:draw]
end
error() click to toggle source

The error message, if any?

# File lib/incline/data_tables_request.rb, line 190
def error
  records
  @config[:error]
end
error?() click to toggle source

Is there an error to display?

# File lib/incline/data_tables_request.rb, line 197
def error?
  !error.blank?
end
length() click to toggle source

Number of records to return.

Can be any positive value, or -1 to indicate that all records should be returned.

# File lib/incline/data_tables_request.rb, line 22
def length
  @config[:length]
end
locate_id() click to toggle source

If we are searching for a specific ID, this is the ID to locate.

# File lib/incline/data_tables_request.rb, line 48
def locate_id
  @config[:locate_id]
end
ordering() click to toggle source

The row ordering.

# File lib/incline/data_tables_request.rb, line 40
def ordering
  @config[:order]
end
provided?() click to toggle source

Where the data tables parameters provided?

# File lib/incline/data_tables_request.rb, line 146
def provided?
  draw != :not_provided
end
record_location(refresh = false) click to toggle source

If locate_id is non-zero, this is the record number for the specified ID using our filtering parameters.

This will be an absolute zero-based record number for the ID. It can be used to figure out the page that the record would appear on.

# File lib/incline/data_tables_request.rb, line 162
def record_location(refresh = false)
  @config[:record_location] = nil if refresh
  @config[:record_location] ||= find_record
end
records(refresh = false) click to toggle source

Gets the records returned by this request.

# File lib/incline/data_tables_request.rb, line 169
def records(refresh = false)
  @config[:records] = nil if refresh
  @config[:records] ||= load_records
end
records_filtered() click to toggle source

Gets the total number of records after filtering.

# File lib/incline/data_tables_request.rb, line 183
def records_filtered
  records
  @config[:records_filtered]
end
records_total() click to toggle source

Gets the total number of records before filtering.

# File lib/incline/data_tables_request.rb, line 176
def records_total
  records
  @config[:records_total]
end
refresh!() click to toggle source

Refreshes the data and returns the request instance.

# File lib/incline/data_tables_request.rb, line 152
def refresh!
  records true
  self
end
start() click to toggle source

First record to return.

# File lib/incline/data_tables_request.rb, line 14
def start
  @config[:start]
end

Private Instance Methods

find_record(relation = nil) click to toggle source

Find the record represented by locate_id.

# File lib/incline/data_tables_request.rb, line 205
def find_record(relation = nil)
  if locate_id.nil? || (locate_id.is_a?(::Numeric) && locate_id == 0) || (locate_id.to_s == '')
    return -1
  end

  dataset = load_records(relation, false)
  return -1 if dataset.blank?

  first_item = dataset.first
  klass = first_item.class

  id_field = klass.respond_to?('primary_key')       ? klass.primary_key   : nil
  id_field ||= first_item.respond_to?('id')         ? 'id'                : nil

  return -1 unless id_field
  if locate_id.is_a?(::Numeric)
    dataset.index{|item| item.send(id_field) == locate_id} || -1
  else
    loc_id = locate_id.to_s.downcase
    dataset.index{|item| item.send(id_field).to_s.downcase == loc_id} || -1
  end

end
get_value(item, name) click to toggle source
# File lib/incline/data_tables_request.rb, line 363
def get_value(item, name)
  val = item
  while item && name.to_s.strip != ''
    top,_,name = name.to_s.partition('.')
    begin
      val = val.respond_to?(top) ? val.send(top) : nil
    rescue =>e
      ::Incline::Log::warn e
      val = nil
    end
  end
  val
end
load_records(relation = nil, paging = true) click to toggle source

Applies the request against an ActiveRecord::Relation object.

Returns the results of executing the necessary queries and filters as an array of models.

# File lib/incline/data_tables_request.rb, line 233
def load_records(relation = nil, paging = true)
  begin
    # reset values.
    # @config[:records] is set to the return of this method, we we won't change that here.
    @config[:records_total] = 0
    @config[:records_filtered] = 0
    @config[:error] = nil

    Incline::Log::debug "Loading records for data tables request #{draw}."

    # Get the default starting scope if necessary.
    relation ||= @config[:starting_scope].call

    # store the unfiltered count.
    @config[:records_total] = relation.count

    # If we have search parameters and any of the search parameters is a regular expression or
    # one or more columns being searched is not a database column, then filtering must be done
    # locally.
    filter_local =
        !(search.blank? && columns.reject{|c| c[:search].blank? }.blank?) &&
            (
                # main search is a regex.
            search.is_a?(::Regexp) ||
                # one or more column searches is a regex.
                columns.select{|c| c[:search].is_a?(::Regexp)}.any? ||
                # one or more searchable columns is not in the database model
                columns.reject {|c| !c[:searchable] || relation.model.column_names.include?(c[:name].to_s) }.any?
            )

    order_local = ordering.blank? || ordering.reject{|k,_| relation.model.column_names.include?(k.to_s)}.any?

    Incline::Log::debug "Filtering will be done #{(filter_local || order_local) ? 'application' : 'database'}-side."
    Incline::Log::debug "Ordering will be done #{order_local ? 'application' : 'database'}-side."

    unless filter_local
      ###  Database Side Individual Filtering  (AND) ###
      columns.reject{|c| c[:search].blank? || c[:name].blank?}.each do |col|
        relation = relation.where("(UPPER(\"#{col[:name]}\") LIKE ?)", "%#{col[:search].upcase}%")
      end

      ###  Database Side Multiple Filtering  (OR) ###
      unless search.blank?
        srch = "%#{search.upcase}%"
        cols = columns.select{|c| c[:searchable]}.map{|c| c[:name].to_s }.reject{|c| c.blank?}
        if cols.any?
          relation = relation.where(
              cols.map{|c| "(UPPER(\"#{c}\") LIKE ?)"}.join(' OR '),
              *(cols.map{ srch })
          )
        end
      end
    end

    ###  Database Side Ordering  ###
    unless order_local
      relation = relation.order(ordering)
    end

    # Now we have two paths, if we are filtering/ordering locally, we need to return everything up to this point and
    # perform our filters before limiting the results.
    # If we filtered and ordered at the database, then we can limit the results there as well.
    if filter_local || order_local
      # execute the query
      relation = relation.to_a

      ###  Application Side Ordering  ###
      if order_local
        ordering_list = ordering.to_a
        relation.sort!{|a,b| local_sort(a, b, ordering_list) }
      end

      ###  Local Individual Filtering   (AND) ###
      columns.reject{|c| c[:search].blank? || c[:name].blank?}.each do |col|
        name = col[:name].to_s
        srch = col[:search]
        relation =
            if srch.is_a?(::Regexp)
              relation.select { |item| get_value(item, name) =~ srch }
            else
              srch = srch.to_s.upcase
              relation.select { |item| get_value(item, name).to_s.upcase.include?(srch) }
            end
      end

      ###  Local Multiple Filtering  ###
      unless search.blank?
        cols = columns.select{|c| c[:searchable]}.map{|c| c[:name].to_s }.reject{|c| c.blank?}
        relation =
            if search.is_a?(::Regexp)
              relation.select{|item| cols.find{|col| get_value(item,col) =~ search} }
            else
              srch = search.to_s.upcase
              relation.select{|item| cols.find{|col| get_value(item,col).to_s.upcase.include?(srch) }}
            end
      end

      # store the filtered count.
      @config[:records_filtered] = relation.count

      if paging
        # apply limits and return.
        relation                   = relation[start..-1]
        if length > 0
          relation = relation[0...length]
        end
      end
      relation
    else
      # store the filtered count.
      @config[:records_filtered] = relation.count

      if paging
        # apply limits and return.
        relation                   = relation.offset(start)
        if length > 0
          relation = relation.limit(length)
        end
      end
      relation.to_a
    end
  rescue =>err
    @config[:error] = err.message
    Incline::Log::error err
    [ ]
  end
end
local_sort(a, b, attribs, index = 0) click to toggle source
# File lib/incline/data_tables_request.rb, line 377
def local_sort(a, b, attribs, index = 0)
  if index >= attribs.count
    if a.class == b.class && a.is_a?(::ActiveRecord::Base)
      pk = a.class.primary_key
      if a.respond_to?(pk)
        a.send(pk) <=> b.send(pk)
      else
        0
      end
    else
      0
    end
  else
    attr, dir = attribs[index]
    val_a = get_value(a, attr)
    val_b = get_value(b, attr)
    if val_a == val_b
      local_sort a, b, attribs, index + 1
    else
      if dir.to_s.downcase == 'desc'
        val_b <=> val_a
      else
        val_a <=> val_b
      end
    end
  end
end