SysMO-DB/seek

View on GitHub
lib/grouped_pagination.rb

Summary

Maintainability
C
7 hrs
Test Coverage
#encoding: utf-8
=begin
Inspired by http://www.hennessynet.com/blog/?p=70
but as a seperate mixin, rather than built into the will_paginate plugin
=end
module GroupedPagination
  def self.included(base)
    base.extend ClassMethods
  end
  
  module ClassMethods
    
    #this is the array of possible pages, defaults to A-Z. Can be set with the options[:pages] in grouped_pagination definition in model
    attr_reader :pages
    
    #this is limit to the list when showing 'latest', 7. Can be set with the options[:latest_limit] in grouped_pagination definition in model
    attr_reader :latest_limit

    #this is the default page to use if :page is not provided when paginating.
    attr_reader :default_page
    
    def grouped_pagination(options={})
      @pages = options[:pages] || ("A".."Z").to_a + ["?"]
      @field = options[:field] || "first_letter"
      @latest_limit = options[:latest_limit] || Seek::Config.limit_latest
      @default_page = options[:default_page] || Seek::Config.default_page(self.name.underscore.pluralize) || 'all'

      before_save :update_first_letter
      
      include GroupedPagination::InstanceMethods
      extend GroupedPagination::SingletonMethods
    end
    
    def paginate_after_fetch(collection, *args)
      options=args.pop unless args.nil?
      options ||= {}
      reorder = options[:reorder].nil? ? true : options[:reorder]

      @latest_limit = options[:latest_limit] || @latest_limit
      @default_page = options[:default_page] || @default_page
      
      default_page = @default_page
      default_page = @pages.first if default_page == "first"      
      
      page = options[:page] || default_page            
    
      records=[]
      if page == "all"
        records=collection
      elsif page == "latest"
        if reorder
          records=collection.sort{|x,y| y.updated_at <=> x.updated_at}[0...@latest_limit]
        else
          records=collection[0...@latest_limit]
        end
      elsif @pages.include?(page)           
        records=collection.select {|i| i.first_letter == page}        
      end
      
      page_totals={}
      @pages.each do |p|
        page_totals[p]=collection.select {|i| i.first_letter == p}.size            
      end
      
      result = Collection.new(records, page, @pages, page_totals)
      
      #jump to the first page with content if no page is specified and their is no content in the first page.
      if (result.empty? && options[:page].nil?)
        first_page_with_content = result.pages.find{|p| result.page_totals[p]>0}
        unless first_page_with_content.nil?
          options[:page]=first_page_with_content
          result=self.paginate_after_fetch(collection, options)
        end
      end
      
      result
    end   

  end
  
  module SingletonMethods
    
    def merge_optional_conditions(optional_conditions, page)
      conditions= []
      conditions << ["#{@field} = ?", page]
      conditions << optional_conditions if optional_conditions
      conditions=merge_conditions(*conditions)
      return conditions
    end
    
    def paginate(*args)
      options=args.pop unless args.nil?
      options ||= {}                

      default_page = options[:default_page] || @default_page
      default_page = @pages.first if default_page == "first"  

      page = options[:page] || default_page
    
      records=[]
      if page == "all"
        records=self.default_order
      elsif page == "latest"
        records=self.unscoped.order("updated_at DESC").limit(@latest_limit)
      elsif @pages.include?(page)
        conditions = merge_optional_conditions(options[:conditions], page)
        query_options = {:conditions=>conditions}
        query_options.merge!(options.except(:conditions,:page,:default_page))
        records=self.unscoped.where(query_options[:conditions]).order(query_options[:order])
      end
      
      page_totals={}
      @pages.each do |p|
        conditions=merge_optional_conditions(options[:conditions],p)
        query_options = [:conditions=>conditions]
        query_options[0].merge!(options.except(:conditions,:page,:default_page))
        page_totals[p]=self.count(*query_options)            
      end
      
      result = Collection.new(records, page, @pages, page_totals)
      
      #jump to the first page with content if no page is specified and their is no content in the first page.
      if (result.empty? && options[:page].nil?)
        first_page_with_content = result.pages.find{|p| result.page_totals[p]>0}
        unless first_page_with_content.nil?
          options[:page]=first_page_with_content
          result=paginate(options)
        end
      end
      
      result
    end

    private
    def merge_conditions(*conditions)
      segments = []

      conditions.each do |condition|
        unless condition.blank?
          sql = sanitize_sql(condition)
          segments << sql unless sql.blank?
        end
      end

      "(#{segments.join(') AND (')})" unless segments.empty?
    end
    
  end
  
  module InstanceMethods
    
    #Helper to strip the first letter from the text, converting non standard A-Z characters to their equivalent, e.g Ø -> O
    #uses some code based upon: http://github.com/grosser/sort_alphabetical/blob/9a8665d17394506c29cce51d8e22af69e2931523/lib/sort_alphabetical.rb with special handling for Ø
    def strip_first_letter text
      
      #handle the characters that can't be handled through normalization
      %w[ØO].each do |s|
        text.gsub!(/[#{s[0..-2]}]/, s[-1..-1])
      end
      
      codepoints = text.mb_chars.normalize(:d).split(//u)
      ascii=codepoints.map(&:to_s).reject{|e| e.length > 1}.join
      
      return ascii.first.capitalize
      
    end
    
    def update_first_letter
      self.first_letter = strip_first_letter(title.strip.gsub(/[\[\]]/,""))
      self.first_letter = "?" unless self.class.pages.include?(self.first_letter)
    end
        
  end
  
  class Collection < Array
    attr_reader :page, :page_totals, :pages
    
    def initialize records, page, pages, page_totals
      super(records)
      @page=page
      @page_totals=page_totals
      @pages=pages
    end
  end
  
end

ActiveRecord::Base.class_eval do
  include GroupedPagination
end