rmg/magic_grid

View on GitHub
lib/magic_grid/collection.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require 'active_support/core_ext'
require 'magic_grid/logger'

module MagicGrid
  class Collection

    DEFAULTS = {
      :per_page               => 30,
      :searchable             => [],
      :search_method          => :search,
      :listener_handler       => nil,
      :default_col            => 0,
      :post_filter            => false,
      :collection_post_filter => true,
      :count                  => nil,
    }

    def initialize(collection, opts = {})
      @collection = collection || []
      @options = opts
      @current_page = 1
      @sorts = []
      @filter_callbacks = []
      @filters = []
      @searches = []
      @post_filters = []
      @post_filter_callbacks = []
      @paginations = []
      @searchable_columns = []
      @@kaminari_class = defined?(Kaminari) ? Kaminari : nil
    end

    delegate :quoted_table_name, :map, :count, :to => :collection

    attr_accessor :searchable_columns
    attr_reader :current_page, :original_count, :total_pages, :per_page,
                :searches
    attr_writer :options
    cattr_accessor :kaminari_class

    def options
      DEFAULTS.merge(@options || {})
    end

    def count_options
      options[:count]
    end

    def self.create_or_reuse(collection, opts = {})
      if collection.is_a?(self)
        collection.options = opts
        collection
      else
        Collection.new(collection, opts)
      end
    end

    def column_names
      @collection.table.columns.map{|c| c[:name]}
    rescue
      MagicGrid.logger.debug("Given collection doesn't respond to #table well: #{$!}")
      []
    end

    def quote_column_name(col)
      if col.is_a? Symbol and @collection.respond_to? :quoted_table_name
        "#{quoted_table_name}.#{@collection.connection.quote_column_name(col.to_s)}"
      else
        col.to_s
      end
    end

    def hash_string
      if @collection.respond_to? :to_sql
        @collection.to_sql.hash.abs.to_s(36)
      else
        options.hash.abs.to_s(36)
      end
    end

    def search_using_builtin(collection, q)
      collection.__send__(options[:search_method], q)
    end

    def search_using_where(collection, q)
      result = collection
      unless searchable_columns.empty?
        begin
          search_cols = searchable_columns.map {|c| c.custom_sql || c.name }
          clauses = search_cols.map {|c| c << " LIKE :search" }.join(" OR ")
          result = collection.where(clauses, {:search => "%#{q}%"})
        rescue
          MagicGrid.logger.debug "Given collection doesn't respond to :where well"
        end
      end
      result
    end

    def sortable?
      @collection.respond_to?(:order)
    end

    def apply_sort(col, dir)
      if sortable? and col.sortable?
        @reduced_collection = nil
        @sorts << "#{col.custom_sql} #{dir}"
      end
    end

    def searchable?
      (filterable? and not searchable_columns.empty?) or
        (options[:search_method] and
         @collection.respond_to? options[:search_method])
    end

    def apply_search(q)
      if q and not q.empty?
        if searchable?
          @reduced_collection = nil
          @searches << q
        else
          MagicGrid.logger.warn "#{self.class.name}: Ignoring searchable fields on collection"
        end
      end
    end

    def perform_search(collection, q)
      search_using_builtin(collection, q)
    rescue
      MagicGrid.logger.debug "Given collection doesn't respond to #{options[:search_method]} well"
      search_using_where(collection, q)
    end

    def filterable?
      @collection.respond_to? :where
    end

    def apply_filter(filters = {})
      if filterable? and not filters.empty?
        @reduced_collection = nil
        @filters << filters
      end
    end

    def apply_filter_callback(callback)
      if callback.respond_to? :call
        @reduced_collection = nil
        @filter_callbacks << callback
      end
    end

    def add_post_filter_callback(callback)
      if callback.respond_to? :call
        @reduced_collection = nil
        @post_filter_callbacks << callback
      end
    end

    def has_post_filter?
      @collection.respond_to? :post_filter
    end

    def enable_post_filter(yes = true)
      @reduced_collection = nil
      if yes and has_post_filter?
        @post_filters << :post_filter
      end
      self
    end

    def count(collection = nil)
      count_or_hash = collection || @collection
      while count_or_hash.respond_to? :count
        count_or_hash = count_or_hash.send :count, *(Array([count_options]).compact)
      end
      count_or_hash
    end

    def per_page=(n)
      @original_count = self.count @collection
      @per_page = n
      if @per_page
        @total_pages = calculate_total_pages(@per_page, @original_count)
      else
        @total_pages = 1
      end
    end

    def apply_pagination(current_page)
      @current_page = current_page
      @reduced_collection = nil
    end

    def default_paginate(collection, page, per_page)
      collection = collection.to_enum
      collection = collection.each_slice(@per_page)
      collection = collection.drop(@current_page - 1)
      collection = collection.first.to_a
      class << collection
        attr_accessor :current_page, :total_pages, :original_count
      end
      collection
    end

    def perform_pagination(collection)
      return collection unless @per_page

      total_entries = count(collection)
      @current_page = bound_current_page(@current_page,
                                         @per_page,
                                         total_entries)

      if collection.respond_to? :paginate
        collection.paginate(:page => @current_page,
                            :per_page => @per_page,
                            :total_entries => total_entries)
      elsif collection.respond_to? :page
        collection.page(@current_page).per(@per_page)
      elsif collection.is_a?(Array) and @@kaminari_class
         @@kaminari_class.paginate_array(collection).
                          page(@current_page).per(@per_page)
      else
         default_paginate(collection, @current_page, @per_page)
      end
    end

    def apply_all_operations(collection)
      @sorts.each do |ordering|
        collection = collection.order(ordering)
      end
      if @filter_callbacks.empty?
        @filters.each do |hsh|
          collection = collection.where(hsh)
        end
      else
        @filter_callbacks.each do |callback|
          collection = callback.call(collection)
        end
      end
      @searches.each do |query|
        collection = perform_search(collection, query)
      end
      # Do collection filter first, may convert from AR to Array
      @post_filters.each do |filter|
        collection = collection.__send__(filter)
      end
      @post_filter_callbacks.each do |callback|
        collection = callback.call(collection)
      end
      # Paginate at the very end, after all sorting, filtering, etc..
      perform_pagination(collection)
    end

    def collection
      @reduced_collection ||= apply_all_operations(@collection)
    end

    def calculate_total_pages(per_page, total_entries)
      pages = total_entries / per_page
      pages += 1 if total_entries % per_page > 0
      if pages < 1
        1
      else
        pages
      end
    end

    def bound_current_page(page, per_page, total_entries)
      pages = calculate_total_pages(per_page, total_entries)
      if page < 1
        1
      elsif page > pages
        pages
      else
        page
      end
    end

  end
end