├── .gemtest ├── .rspec ├── lib ├── kaminari │ ├── version.rb │ ├── grape.rb │ ├── engine.rb │ ├── sinatra.rb │ ├── railtie.rb │ ├── models │ │ ├── data_mapper_collection_methods.rb │ │ ├── plucky_criteria_methods.rb │ │ ├── mongoid_criteria_methods.rb │ │ ├── mongo_mapper_extension.rb │ │ ├── active_record_model_extension.rb │ │ ├── active_record_extension.rb │ │ ├── mongoid_extension.rb │ │ ├── page_scope_methods.rb │ │ ├── configuration_methods.rb │ │ ├── active_record_relation_methods.rb │ │ ├── data_mapper_extension.rb │ │ └── array_extension.rb │ ├── config.rb │ ├── hooks.rb │ └── helpers │ │ ├── tags.rb │ │ ├── sinatra_helpers.rb │ │ ├── paginator.rb │ │ └── action_view_extension.rb ├── generators │ └── kaminari │ │ ├── templates │ │ └── kaminari_config.rb │ │ ├── config_generator.rb │ │ └── views_generator.rb └── kaminari.rb ├── .document ├── spec ├── fake_app │ ├── data_mapper │ │ ├── config.rb │ │ └── models.rb │ ├── mongo_mapper │ │ ├── config.rb │ │ └── models.rb │ ├── active_record │ │ ├── config.rb │ │ └── models.rb │ ├── mongoid │ │ ├── models.rb │ │ └── config.rb │ ├── sinatra_app.rb │ └── rails_app.rb ├── fake_gem.rb ├── support │ ├── database_cleaner.rb │ └── matchers.rb ├── spec_helper.rb ├── models │ ├── active_record │ │ ├── default_per_page_spec.rb │ │ ├── max_per_page_spec.rb │ │ ├── active_record_relation_methods_spec.rb │ │ └── scopes_spec.rb │ ├── mongo_mapper │ │ └── mongo_mapper_spec.rb │ ├── array_spec.rb │ ├── mongoid │ │ └── mongoid_spec.rb │ └── data_mapper │ │ └── data_mapper_spec.rb ├── spec_helper_for_sinatra.rb ├── requests │ └── users_spec.rb ├── config │ └── config_spec.rb └── helpers │ ├── helpers_spec.rb │ ├── sinatra_helpers_spec.rb │ ├── tags_spec.rb │ └── action_view_extension_spec.rb ├── Gemfile ├── .gitignore ├── gemfiles ├── mongoid_24.gemfile ├── mongoid_30.gemfile ├── mongo_mapper.gemfile ├── active_record_31.gemfile ├── active_record_32.gemfile ├── active_record_30.gemfile ├── sinatra.gemfile └── data_mapper_12.gemfile ├── app └── views │ └── kaminari │ ├── _gap.html.haml │ ├── _gap.html.slim │ ├── _gap.html.erb │ ├── _last_page.html.slim │ ├── _first_page.html.slim │ ├── _last_page.html.haml │ ├── _first_page.html.haml │ ├── _next_page.html.slim │ ├── _next_page.html.haml │ ├── _prev_page.html.slim │ ├── _last_page.html.erb │ ├── _prev_page.html.haml │ ├── _first_page.html.erb │ ├── _next_page.html.erb │ ├── _prev_page.html.erb │ ├── _page.html.slim │ ├── _page.html.haml │ ├── _page.html.erb │ ├── _paginator.html.haml │ ├── _paginator.html.slim │ └── _paginator.html.erb ├── .travis.yml ├── config └── locales │ └── kaminari.yml ├── MIT-LICENSE ├── Rakefile ├── kaminari.gemspec ├── CHANGELOG └── README.rdoc /.gemtest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=d 3 | -------------------------------------------------------------------------------- /lib/kaminari/version.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | VERSION = '0.14.1' 3 | end 4 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /spec/fake_app/data_mapper/config.rb: -------------------------------------------------------------------------------- 1 | DataMapper.setup(:default, 'sqlite::memory:') 2 | -------------------------------------------------------------------------------- /lib/kaminari/grape.rb: -------------------------------------------------------------------------------- 1 | require 'grape' 2 | require 'kaminari' 3 | 4 | Kaminari::Hooks.init 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in kaminari.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/kaminari/engine.rb: -------------------------------------------------------------------------------- 1 | module Kaminari #:nodoc: 2 | class Engine < ::Rails::Engine #:nodoc: 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .idea 4 | Gemfile.lock 5 | gemfiles/*.lock 6 | pkg/* 7 | 8 | doc 9 | log 10 | -------------------------------------------------------------------------------- /lib/kaminari/sinatra.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'kaminari' 3 | require 'kaminari/helpers/sinatra_helpers' 4 | 5 | Kaminari::Hooks.init 6 | -------------------------------------------------------------------------------- /spec/fake_app/mongo_mapper/config.rb: -------------------------------------------------------------------------------- 1 | MongoMapper.connection = Mongo::Connection.new 'localhost', 27017 2 | MongoMapper.database = 'kaminari_test' 3 | -------------------------------------------------------------------------------- /spec/fake_gem.rb: -------------------------------------------------------------------------------- 1 | # Simulate a gem providing a subclass of ActiveRecord::Base before the Railtie is loaded. 2 | 3 | class GemDefinedModel < ActiveRecord::Base 4 | end 5 | -------------------------------------------------------------------------------- /gemfiles/mongoid_24.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.2.3' 4 | gem 'mongoid', '~> 2.4.0' 5 | gem 'rspec-rails', '>= 2.0' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /gemfiles/mongoid_30.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.2.3' 4 | gem 'mongoid', '>= 3.0' 5 | gem 'rspec-rails', '>= 2.0' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /gemfiles/mongo_mapper.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.2.3' 4 | gem 'mongo_mapper', '>= 0.11.0' 5 | gem 'rspec-rails', '>= 2.0' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /lib/kaminari/railtie.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | class Railtie < ::Rails::Railtie #:nodoc: 3 | initializer 'kaminari' do |_app| 4 | Kaminari::Hooks.init 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fake_app/active_record/config.rb: -------------------------------------------------------------------------------- 1 | # database 2 | ActiveRecord::Base.configurations = {'test' => {:adapter => 'sqlite3', :database => ':memory:'}} 3 | ActiveRecord::Base.establish_connection('test') 4 | -------------------------------------------------------------------------------- /gemfiles/active_record_31.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.1.5' 4 | gem 'activerecord', '>= 3.1.5', :require => 'active_record' 5 | gem 'rspec-rails', '>= 2.0' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /gemfiles/active_record_32.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.2.3' 4 | gem 'activerecord', '>= 3.2.3', :require => 'active_record' 5 | gem 'rspec-rails', '>= 2.0' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /gemfiles/active_record_30.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.0.13' 4 | gem 'activerecord', '>= 3.0.13', :require => 'active_record' 5 | gem 'rspec-rails', '>= 2.0' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /spec/fake_app/mongo_mapper/models.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include ::MongoMapper::Document 3 | key :name, String 4 | key :age, Integer 5 | end 6 | 7 | class User::Address 8 | include ::MongoMapper::Document 9 | end 10 | -------------------------------------------------------------------------------- /gemfiles/sinatra.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'activerecord', '>= 3.2.3', :require => 'active_record' 4 | gem 'sinatra', '>= 1.3' 5 | gem 'padrino-helpers', '>= 0.10.6.c' 6 | gem 'rack-test', '>= 0' 7 | gem 'sinatra-contrib', '>= 1.3' 8 | gem 'nokogiri', '>= 0' 9 | 10 | gemspec :path => '../' 11 | -------------------------------------------------------------------------------- /lib/generators/kaminari/templates/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | Kaminari.configure do |config| 2 | # config.default_per_page = 25 3 | # config.max_per_page = nil 4 | # config.window = 4 5 | # config.outer_window = 0 6 | # config.left = 0 7 | # config.right = 0 8 | # config.page_method_name = :page 9 | # config.param_name = :page 10 | end 11 | -------------------------------------------------------------------------------- /gemfiles/data_mapper_12.gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'railties', '>= 3.2.3' 4 | gem 'dm-core', '>= 1.2.0' 5 | gem 'dm-migrations', '>= 1.2.0' 6 | gem 'dm-aggregates', '>= 1.2.0' 7 | gem 'dm-transactions', '>= 1.2.0' 8 | gem 'dm-active_model', '>= 1.2.0' 9 | gem 'dm-sqlite-adapter', '>= 1.2.0' 10 | gem 'rspec-rails', '>= 2.0' 11 | 12 | gemspec :path => '../' 13 | -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.haml: -------------------------------------------------------------------------------- 1 | -# Non-link tag that stands for skipped pages... 2 | -# available local variables 3 | -# current_page: a page object for the currently displayed page 4 | -# total_pages: total number of pages 5 | -# per_page: number of items to fetch per page 6 | -# remote: data-remote 7 | %span.page.gap 8 | = raw(t 'views.pagination.truncate') 9 | -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.slim: -------------------------------------------------------------------------------- 1 | / Non-link tag that stands for skipped pages... 2 | - available local variables 3 | current_page : a page object for the currently displayed page 4 | total_pages : total number of pages 5 | per_page : number of items to fetch per page 6 | remote : data-remote 7 | span.page.gap 8 | == raw(t 'views.pagination.truncate') 9 | ' 10 | -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.erb: -------------------------------------------------------------------------------- 1 | <%# Non-link tag that stands for skipped pages... 2 | - available local variables 3 | current_page: a page object for the currently displayed page 4 | total_pages: total number of pages 5 | per_page: number of items to fetch per page 6 | remote: data-remote 7 | -%> 8 | <%= raw(t 'views.pagination.truncate') %> 9 | -------------------------------------------------------------------------------- /lib/kaminari/models/data_mapper_collection_methods.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module DataMapperCollectionMethods 3 | def limit_value #:nodoc: 4 | query.options[:limit] || 0 5 | end 6 | 7 | def offset_value #:nodoc: 8 | query.options[:offset] || 0 9 | end 10 | 11 | def total_count #:nodoc: 12 | model.count(query.options.except(:limit, :offset, :order)) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/kaminari/models/plucky_criteria_methods.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module PluckyCriteriaMethods 3 | include Kaminari::PageScopeMethods 4 | include Kaminari::ConfigurationMethods::ClassMethods 5 | 6 | def limit_value #:nodoc: 7 | options[:limit] 8 | end 9 | 10 | def offset_value #:nodoc: 11 | options[:skip] 12 | end 13 | 14 | def total_count #:nodoc: 15 | count 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.slim: -------------------------------------------------------------------------------- 1 | / Link to the "Last" page 2 | - available local variables 3 | url : url to the last page 4 | current_page : a page object for the currently displayed page 5 | total_pages : total number of pages 6 | per_page : number of items to fetch per page 7 | remote : data-remote 8 | span.last 9 | == link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, :remote => remote 10 | ' 11 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.slim: -------------------------------------------------------------------------------- 1 | / Link to the "First" page 2 | - available local variables 3 | url : url to the first page 4 | current_page : a page object for the currently displayed page 5 | total_pages : total number of pages 6 | per_page : number of items to fetch per page 7 | remote : data-remote 8 | span.first 9 | == link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote 10 | ' 11 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.haml: -------------------------------------------------------------------------------- 1 | -# Link to the "Last" page 2 | -# available local variables 3 | -# url: url to the last page 4 | -# current_page: a page object for the currently displayed page 5 | -# total_pages: total number of pages 6 | -# per_page: number of items to fetch per page 7 | -# remote: data-remote 8 | %span.last 9 | = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, :remote => remote 10 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.haml: -------------------------------------------------------------------------------- 1 | -# Link to the "First" page 2 | -# available local variables 3 | -# url: url to the first page 4 | -# current_page: a page object for the currently displayed page 5 | -# total_pages: total number of pages 6 | -# per_page: number of items to fetch per page 7 | -# remote: data-remote 8 | %span.first 9 | = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote 10 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.slim: -------------------------------------------------------------------------------- 1 | / Link to the "Next" page 2 | - available local variables 3 | url : url to the next page 4 | current_page : a page object for the currently displayed page 5 | total_pages : total number of pages 6 | per_page : number of items to fetch per page 7 | remote : data-remote 8 | span.next 9 | == link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote 10 | ' 11 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.haml: -------------------------------------------------------------------------------- 1 | -# Link to the "Next" page 2 | -# available local variables 3 | -# url: url to the next page 4 | -# current_page: a page object for the currently displayed page 5 | -# total_pages: total number of pages 6 | -# per_page: number of items to fetch per page 7 | -# remote: data-remote 8 | %span.next 9 | = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote 10 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.slim: -------------------------------------------------------------------------------- 1 | / Link to the "Previous" page 2 | - available local variables 3 | url : url to the previous page 4 | current_page : a page object for the currently displayed page 5 | total_pages : total number of pages 6 | per_page : number of items to fetch per page 7 | remote : data-remote 8 | span.prev 9 | == link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote 10 | ' 11 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Last" page 2 | - available local variables 3 | url: url to the last page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, :remote => remote %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.haml: -------------------------------------------------------------------------------- 1 | -# Link to the "Previous" page 2 | -# available local variables 3 | -# url: url to the previous page 4 | -# current_page: a page object for the currently displayed page 5 | -# total_pages: total number of pages 6 | -# per_page: number of items to fetch per page 7 | -# remote: data-remote 8 | %span.prev 9 | = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote 10 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "First" page 2 | - available local variables 3 | url: url to the first page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Next" page 2 | - available local variables 3 | url: url to the next page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link to the "Previous" page 2 | - available local variables 3 | url: url to the previous page 4 | current_page: a page object for the currently displayed page 5 | total_pages: total number of pages 6 | per_page: number of items to fetch per page 7 | remote: data-remote 8 | -%> 9 | 10 | <%= link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote %> 11 | 12 | -------------------------------------------------------------------------------- /lib/generators/kaminari/config_generator.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module Generators 3 | class ConfigGenerator < Rails::Generators::Base 4 | source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates')) 5 | 6 | desc < remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} 11 | ' 12 | -------------------------------------------------------------------------------- /spec/fake_app/mongoid/models.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include ::Mongoid::Document 3 | field :name, :type => String 4 | field :age, :type => Integer 5 | end 6 | 7 | class User::Address 8 | include ::Mongoid::Document 9 | end 10 | 11 | class MongoMongoidExtensionDeveloper 12 | include ::Mongoid::Document 13 | field :salary, :type => Integer 14 | embeds_many :frameworks 15 | end 16 | 17 | class Framework 18 | include ::Mongoid::Document 19 | field :name, :type => String 20 | field :language, :type => String 21 | embedded_in :mongo_mongoid_extension_developer 22 | end 23 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.haml: -------------------------------------------------------------------------------- 1 | -# Link showing page number 2 | -# available local variables 3 | -# page: a page object for "this" page 4 | -# url: url to this page 5 | -# current_page: a page object for the currently displayed page 6 | -# total_pages: total number of pages 7 | -# per_page: number of items to fetch per page 8 | -# remote: data-remote 9 | %span{:class => "page#{' current' if page.current?}"} 10 | = link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} 11 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.erb: -------------------------------------------------------------------------------- 1 | <%# Link showing page number 2 | - available local variables 3 | page: a page object for "this" page 4 | url: url to this page 5 | current_page: a page object for the currently displayed page 6 | total_pages: total number of pages 7 | per_page: number of items to fetch per page 8 | remote: data-remote 9 | -%> 10 | 11 | <%= link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} %> 12 | 13 | -------------------------------------------------------------------------------- /lib/kaminari/models/mongo_mapper_extension.rb: -------------------------------------------------------------------------------- 1 | require 'kaminari/models/plucky_criteria_methods' 2 | 3 | module Kaminari 4 | module MongoMapperExtension 5 | module Document 6 | extend ActiveSupport::Concern 7 | include Kaminari::ConfigurationMethods 8 | 9 | included do 10 | # Fetch the values at the specified page number 11 | # Model.page(5) 12 | scope Kaminari.config.page_method_name, Proc.new {|num| 13 | limit(default_per_page).offset(default_per_page * ([num.to_i, 1].max - 1)) 14 | } 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/fake_app/data_mapper/models.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include ::DataMapper::Resource 3 | 4 | property :id, Serial 5 | property :name, String, :required => true 6 | property :age, Integer 7 | 8 | has n, :projects, :through => Resource 9 | end 10 | 11 | class User::Address 12 | include ::DataMapper::Resource 13 | 14 | property :id, Serial 15 | end 16 | 17 | class Project 18 | include ::DataMapper::Resource 19 | 20 | property :id, Serial 21 | property :name, String, :required => true 22 | 23 | has n, :users, :through => Resource 24 | end 25 | 26 | DataMapper.finalize 27 | DataMapper.auto_migrate! 28 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | DatabaseCleaner[:active_record].strategy = :transaction if defined? ActiveRecord 2 | DatabaseCleaner[:data_mapper].strategy = :transaction if defined? DataMapper 3 | DatabaseCleaner[:mongoid].strategy = :truncation if defined? Mongoid 4 | DatabaseCleaner[:mongo_mapper].strategy = :truncation if defined? MongoMapper 5 | 6 | RSpec.configure do |config| 7 | config.before :suite do 8 | DatabaseCleaner.clean_with :truncation 9 | end 10 | config.before :each do 11 | DatabaseCleaner.start 12 | end 13 | config.after :each do 14 | DatabaseCleaner.clean 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/fake_app/mongoid/config.rb: -------------------------------------------------------------------------------- 1 | # Ensure we use 'syck' instead of 'psych' in 1.9.2 2 | # RubyGems >= 1.5.0 uses 'psych' on 1.9.2, but 3 | # Psych does not yet support YAML 1.1 merge keys. 4 | # Merge keys is often used in mongoid.yml 5 | # See: http://redmine.ruby-lang.org/issues/show/4300 6 | if RUBY_VERSION >= '1.9.2' 7 | YAML::ENGINE.yamler = 'syck' 8 | end 9 | 10 | Mongoid.configure do |config| 11 | if Mongoid::VERSION =~ /^3/ 12 | config.sessions = {:default => {:hosts => ['localhost:27017'], :database => 'kaminari_test'}} 13 | else 14 | config.master = Mongo::Connection.new.db('kaminari_test') 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/kaminari/models/active_record_model_extension.rb: -------------------------------------------------------------------------------- 1 | require 'kaminari/models/active_record_relation_methods' 2 | 3 | module Kaminari 4 | module ActiveRecordModelExtension 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | self.send(:include, Kaminari::ConfigurationMethods) 9 | 10 | # Fetch the values at the specified page number 11 | # Model.page(5) 12 | self.scope Kaminari.config.page_method_name, Proc.new {|num| 13 | limit(default_per_page).offset(default_per_page * ([num.to_i, 1].max - 1)) 14 | } do 15 | include Kaminari::ActiveRecordRelationMethods 16 | include Kaminari::PageScopeMethods 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/locales/kaminari.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for Kaminari. You can override these values in your app's locales file if you want. 2 | 3 | en: 4 | views: 5 | pagination: 6 | first: "« First" 7 | last: "Last »" 8 | previous: "‹ Prev" 9 | next: "Next ›" 10 | truncate: "…" 11 | helpers: 12 | page_entries_info: 13 | one_page: 14 | display_entries: 15 | zero: "No %{entry_name} found" 16 | one: "Displaying 1 %{entry_name}" 17 | other: "Displaying all %{count} %{entry_name}" 18 | more_pages: 19 | display_entries: "Displaying %{entry_name} %{first} - %{last} of %{total} in total" 20 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.haml: -------------------------------------------------------------------------------- 1 | -# The container tag 2 | -# available local variables 3 | -# current_page: a page object for the currently displayed page 4 | -# total_pages: total number of pages 5 | -# per_page: number of items to fetch per page 6 | -# remote: data-remote 7 | -# paginator: the paginator that renders the pagination tags inside 8 | = paginator.render do 9 | %nav.pagination 10 | = first_page_tag unless current_page.first? 11 | = prev_page_tag unless current_page.first? 12 | - each_page do |page| 13 | - if page.left_outer? || page.right_outer? || page.inside_window? 14 | = page_tag page 15 | - elsif !page.was_truncated? 16 | = gap_tag 17 | = next_page_tag unless current_page.last? 18 | = last_page_tag unless current_page.last? 19 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.slim: -------------------------------------------------------------------------------- 1 | / The container tag 2 | - available local variables 3 | current_page : a page object for the currently displayed page 4 | total_pages : total number of pages 5 | per_page : number of items to fetch per page 6 | remote : data-remote 7 | paginator : the paginator that renders the pagination tags inside 8 | 9 | == paginator.render do 10 | nav.pagination 11 | == first_page_tag unless current_page.first? 12 | == prev_page_tag unless current_page.first? 13 | - each_page do |page| 14 | - if page.left_outer? || page.right_outer? || page.inside_window? 15 | == page_tag page 16 | - elsif !page.was_truncated? 17 | == gap_tag 18 | == next_page_tag unless current_page.last? 19 | == last_page_tag unless current_page.last? 20 | -------------------------------------------------------------------------------- /spec/fake_app/sinatra_app.rb: -------------------------------------------------------------------------------- 1 | require 'fake_app/active_record/config' if defined? ActiveRecord 2 | require 'fake_app/data_mapper/config' if defined? DataMapper 3 | require 'fake_app/mongoid/config' if defined? Mongoid 4 | require 'fake_app/mongo_mapper/config' if defined? MongoMapper 5 | 6 | #models 7 | require 'fake_app/active_record/models' if defined? ActiveRecord 8 | require 'fake_app/data_mapper/models' if defined? DataMapper 9 | require 'fake_app/mongoid/models' if defined? Mongoid 10 | require 'fake_app/mongo_mapper/models' if defined? MongoMapper 11 | 12 | class SinatraApp < Sinatra::Base 13 | register Kaminari::Helpers::SinatraHelpers 14 | 15 | get '/users' do 16 | @users = User.page params[:page] 17 | erb <<-ERB 18 | <%= @users.map(&:name).join("\n") %> 19 | <%= paginate @users %> 20 | ERB 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/kaminari/models/active_record_extension.rb: -------------------------------------------------------------------------------- 1 | require 'kaminari/models/active_record_model_extension' 2 | 3 | module Kaminari 4 | module ActiveRecordExtension 5 | extend ActiveSupport::Concern 6 | included do 7 | # Future subclasses will pick up the model extension 8 | class << self 9 | def inherited_with_kaminari(kls) #:nodoc: 10 | inherited_without_kaminari kls 11 | kls.send(:include, Kaminari::ActiveRecordModelExtension) if kls.superclass == ActiveRecord::Base 12 | end 13 | alias_method_chain :inherited, :kaminari 14 | end 15 | 16 | # Existing subclasses pick up the model extension as well 17 | self.descendants.each do |kls| 18 | kls.send(:include, Kaminari::ActiveRecordModelExtension) if kls.superclass == ActiveRecord::Base 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | begin 5 | require 'rails' 6 | rescue LoadError 7 | end 8 | 9 | require 'bundler/setup' 10 | Bundler.require 11 | 12 | require 'database_cleaner' 13 | 14 | # Simulate a gem providing a subclass of ActiveRecord::Base before the Railtie is loaded. 15 | require 'fake_gem' if defined? ActiveRecord 16 | 17 | if defined? Rails 18 | require 'fake_app/rails_app' 19 | 20 | require 'rspec/rails' 21 | end 22 | if defined? Sinatra 23 | require 'spec_helper_for_sinatra' 24 | end 25 | 26 | # Requires supporting files with custom matchers and macros, etc, 27 | # in ./support/ and its subdirectories. 28 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 29 | 30 | RSpec.configure do |config| 31 | config.mock_with :rr 32 | end 33 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.erb: -------------------------------------------------------------------------------- 1 | <%# The container tag 2 | - available local variables 3 | current_page: a page object for the currently displayed page 4 | total_pages: total number of pages 5 | per_page: number of items to fetch per page 6 | remote: data-remote 7 | paginator: the paginator that renders the pagination tags inside 8 | -%> 9 | <%= paginator.render do -%> 10 | 23 | <% end -%> 24 | -------------------------------------------------------------------------------- /spec/models/active_record/default_per_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? ActiveRecord 4 | 5 | describe 'default per_page' do 6 | describe 'AR::Base' do 7 | subject { ActiveRecord::Base } 8 | it { should_not respond_to :paginates_per } 9 | end 10 | 11 | subject { User.page 0 } 12 | 13 | context 'by default' do 14 | its(:limit_value) { should == 25 } 15 | end 16 | 17 | context 'when explicitly set via paginates_per' do 18 | before { User.paginates_per 1326 } 19 | its(:limit_value) { should == 1326 } 20 | after { User.paginates_per nil } 21 | end 22 | 23 | describe "default per_page value's independency per model" do 24 | context "when User's default per_page was changed" do 25 | before { User.paginates_per 1326 } 26 | subject { Book.page 0 } 27 | its(:limit_value) { should == 25 } 28 | after { User.paginates_per nil } 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/kaminari.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | end 3 | 4 | # load Rails/Railtie 5 | begin 6 | require 'rails' 7 | rescue LoadError 8 | #do nothing 9 | end 10 | 11 | $stderr.puts <<-EOC if !defined?(Rails) && !defined?(Sinatra) && !defined?(Grape) 12 | warning: no framework detected. 13 | 14 | Your Gemfile might not be configured properly. 15 | ---- e.g. ---- 16 | Rails: 17 | gem 'kaminari' 18 | 19 | Sinatra/Padrino: 20 | gem 'kaminari', :require => 'kaminari/sinatra' 21 | 22 | Grape: 23 | gem 'kaminari', :require => 'kaminari/grape' 24 | 25 | EOC 26 | 27 | # load Kaminari components 28 | require 'kaminari/config' 29 | require 'kaminari/helpers/action_view_extension' 30 | require 'kaminari/helpers/paginator' 31 | require 'kaminari/models/page_scope_methods' 32 | require 'kaminari/models/configuration_methods' 33 | require 'kaminari/hooks' 34 | 35 | # if not using Railtie, call `Kaminari::Hooks.init` directly 36 | if defined? Rails 37 | require 'kaminari/railtie' 38 | require 'kaminari/engine' 39 | end 40 | -------------------------------------------------------------------------------- /spec/models/active_record/max_per_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? ActiveRecord 4 | 5 | describe 'max per_page' do 6 | describe 'AR::Base' do 7 | subject { ActiveRecord::Base } 8 | it { should_not respond_to :max_paginates_per } 9 | end 10 | 11 | subject { User.page(0).per(100) } 12 | 13 | context 'by default' do 14 | its(:limit_value) { should == 100 } 15 | end 16 | 17 | context 'when explicitly set via max_paginates_per' do 18 | before { User.max_paginates_per 10 } 19 | its(:limit_value) { should == 10 } 20 | after { User.max_paginates_per nil } 21 | end 22 | 23 | describe "max per_page value's independency per model" do 24 | context "when User's max per_page was changed" do 25 | before { User.max_paginates_per 10 } 26 | subject { Book.page(0).per(100) } 27 | its(:limit_value) { should == 100 } 28 | after { User.max_paginates_per nil } 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/kaminari/models/mongoid_extension.rb: -------------------------------------------------------------------------------- 1 | require 'kaminari/models/mongoid_criteria_methods' 2 | 3 | module Kaminari 4 | module MongoidExtension 5 | module Criteria 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 10 | def #{Kaminari.config.page_method_name}(*args) 11 | super(*args).criteria.merge(self) 12 | end 13 | RUBY 14 | end 15 | end 16 | 17 | module Document 18 | extend ActiveSupport::Concern 19 | include Kaminari::ConfigurationMethods 20 | 21 | included do 22 | # Fetch the values at the specified page number 23 | # Model.page(5) 24 | scope Kaminari.config.page_method_name, Proc.new {|num| 25 | limit(default_per_page).offset(default_per_page * ([num.to_i, 1].max - 1)) 26 | } do 27 | include Kaminari::MongoidCriteriaMethods 28 | include Kaminari::PageScopeMethods 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper_for_sinatra.rb: -------------------------------------------------------------------------------- 1 | require 'kaminari/sinatra' 2 | require 'rack/test' 3 | require 'sinatra/test_helpers' 4 | require 'capybara/dsl' 5 | require 'capybara/rspec' 6 | 7 | require 'fake_app/sinatra_app' 8 | 9 | Capybara.app = SinatraApp 10 | 11 | module HelperMethodForHelperSpec 12 | module FakeEnv 13 | def env 14 | {'PATH_INFO' => '/'} 15 | end 16 | end 17 | 18 | def helper 19 | # OMG terrible object... 20 | Kaminari::Helpers::SinatraHelpers::ActionViewTemplateProxy.new(:current_params => {}, :current_path => '/', :param_name => Kaminari.config.param_name).extend(Padrino::Helpers, Kaminari::ActionViewExtension, Kaminari::Helpers::SinatraHelpers::HelperMethods, FakeEnv) 21 | end 22 | end 23 | 24 | RSpec.configure do |config| 25 | config.include Rack::Test::Methods 26 | config.include Sinatra::TestHelpers 27 | config.include Capybara::DSL 28 | config.include HelperMethodForHelperSpec 29 | # config.include HelperMethodForHelperSpec, :type => :helper 30 | end 31 | 32 | require 'nokogiri' 33 | def last_document 34 | Nokogiri::HTML(last_response.body) 35 | end 36 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Akira Matsuda 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/kaminari/models/page_scope_methods.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module PageScopeMethods 3 | # Specify the per_page value for the preceding page scope 4 | # Model.page(3).per(10) 5 | def per(num) 6 | if (n = num.to_i) <= 0 7 | limit(0) 8 | elsif max_per_page && max_per_page < n 9 | limit(max_per_page).offset(offset_value / limit_value * max_per_page) 10 | else 11 | limit(n).offset(offset_value / limit_value * n) 12 | end 13 | end 14 | 15 | def padding(num) 16 | offset(offset_value + num.to_i) 17 | end 18 | 19 | # Total number of pages 20 | def total_pages 21 | (total_count.to_f / limit_value).ceil 22 | end 23 | #FIXME for compatibility. remove num_pages at some time in the future 24 | alias num_pages total_pages 25 | 26 | # Current page number 27 | def current_page 28 | (offset_value / limit_value) + 1 29 | end 30 | 31 | # First page of the collection ? 32 | def first_page? 33 | current_page == 1 34 | end 35 | 36 | # Last page of the collection? 37 | def last_page? 38 | current_page >= total_pages 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/kaminari/models/configuration_methods.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module ConfigurationMethods 3 | extend ActiveSupport::Concern 4 | module ClassMethods 5 | # Overrides the default +per_page+ value per model 6 | # class Article < ActiveRecord::Base 7 | # paginates_per 10 8 | # end 9 | def paginates_per(val) 10 | @_default_per_page = val 11 | end 12 | 13 | # This model's default +per_page+ value 14 | # returns +default_per_page+ value unless explicitly overridden via paginates_per 15 | def default_per_page 16 | @_default_per_page ||= Kaminari.config.default_per_page 17 | end 18 | 19 | # Overrides the max +per_page+ value per model 20 | # class Article < ActiveRecord::Base 21 | # max_paginates_per 100 22 | # end 23 | def max_paginates_per(val) 24 | @_max_per_page = val 25 | end 26 | 27 | # This model's max +per_page+ value 28 | # returns +max_per_page+ value unless explicitly overridden via max_paginates_per 29 | def max_per_page 30 | @_max_per_page || Kaminari.config.max_per_page 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/kaminari/models/active_record_relation_methods.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module ActiveRecordRelationMethods 3 | # a workaround for AR 3.0.x that returns 0 for #count when page > 1 4 | # if +limit_value+ is specified, load all the records and count them 5 | if ActiveRecord::VERSION::STRING < '3.1' 6 | def count(column_name = nil, options = {}) #:nodoc: 7 | limit_value ? length : super(column_name, options) 8 | end 9 | end 10 | 11 | def total_count(column_name = nil, options = {}) #:nodoc: 12 | # #count overrides the #select which could include generated columns referenced in #order, so skip #order here, where it's irrelevant to the result anyway 13 | @total_count ||= begin 14 | c = except(:offset, :limit, :order) 15 | 16 | # Remove includes only if they are irrelevant 17 | c = c.except(:includes) unless references_eager_loaded_tables? 18 | 19 | # .group returns an OrderdHash that responds to #count 20 | c = c.count(column_name, options) 21 | if c.is_a?(ActiveSupport::OrderedHash) 22 | c.count 23 | else 24 | c.respond_to?(:count) ? c.count(column_name, options) : c 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/kaminari/models/data_mapper_extension.rb: -------------------------------------------------------------------------------- 1 | require 'kaminari/models/data_mapper_collection_methods' 2 | 3 | module Kaminari 4 | module DataMapperExtension 5 | module Paginatable 6 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 7 | def #{Kaminari.config.page_method_name}(num = 1) 8 | num = [num.to_i, 1].max - 1 9 | all(:limit => default_per_page, :offset => default_per_page * num).extend Paginating 10 | end 11 | RUBY 12 | end 13 | 14 | module Paginating 15 | include Kaminari::PageScopeMethods 16 | 17 | def all(options={}) 18 | super.extend Paginating 19 | end 20 | 21 | def per(num) 22 | super.extend Paginating 23 | end 24 | end 25 | 26 | module Collection 27 | extend ActiveSupport::Concern 28 | included do 29 | include Kaminari::ConfigurationMethods::ClassMethods 30 | include Kaminari::DataMapperCollectionMethods 31 | include Paginatable 32 | end 33 | end 34 | 35 | module Model 36 | include Kaminari::ConfigurationMethods::ClassMethods 37 | include Paginatable 38 | 39 | def limit(val) 40 | all(:limit => val) 41 | end 42 | 43 | def offset(val) 44 | all(:offset => val) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/requests/users_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'spec_helper' 3 | 4 | feature 'Users' do 5 | background do 6 | 1.upto(100) {|i| User.create! :name => "user#{'%03d' % i}" } 7 | end 8 | scenario 'navigating by pagination links' do 9 | visit '/users' 10 | 11 | within 'nav.pagination' do 12 | within 'span.page.current' do 13 | page.should have_content '1' 14 | end 15 | within 'span.next' do 16 | click_link 'Next ›' 17 | end 18 | end 19 | 20 | within 'nav.pagination' do 21 | within 'span.page.current' do 22 | page.should have_content '2' 23 | end 24 | within 'span.last' do 25 | click_link 'Last »' 26 | end 27 | end 28 | 29 | within 'nav.pagination' do 30 | within 'span.page.current' do 31 | page.should have_content '4' 32 | end 33 | within 'span.prev' do 34 | click_link '‹ Prev' 35 | end 36 | end 37 | 38 | within 'nav.pagination' do 39 | within 'span.page.current' do 40 | page.should have_content '3' 41 | end 42 | within 'span.first' do 43 | click_link '« First' 44 | end 45 | end 46 | 47 | within 'nav.pagination' do 48 | within 'span.page.current' do 49 | page.should have_content '1' 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'bundler' 4 | Bundler::GemHelper.install_tasks 5 | 6 | require 'rspec/core' 7 | require 'rspec/core/rake_task' 8 | 9 | RSpec::Core::RakeTask.new(:spec) do |spec| 10 | spec.pattern = FileList['spec/**/*_spec.rb'] 11 | end 12 | 13 | task :default => "spec:all" 14 | 15 | namespace :spec do 16 | %w(active_record_32 active_record_31 active_record_30 data_mapper_12 mongoid_30 mongoid_24 mongo_mapper sinatra).each do |gemfile| 17 | desc "Run Tests against #{gemfile}" 18 | task gemfile do 19 | sh "BUNDLE_GEMFILE='gemfiles/#{gemfile}.gemfile' bundle --quiet" 20 | sh "BUNDLE_GEMFILE='gemfiles/#{gemfile}.gemfile' bundle exec rake -t spec" 21 | end 22 | end 23 | 24 | desc "Run Tests against all ORMs" 25 | task :all do 26 | %w(active_record_32 active_record_31 active_record_30 data_mapper_12 mongoid_30 mongoid_24 mongo_mapper sinatra).each do |gemfile| 27 | sh "BUNDLE_GEMFILE='gemfiles/#{gemfile}.gemfile' bundle --quiet" 28 | sh "BUNDLE_GEMFILE='gemfiles/#{gemfile}.gemfile' bundle exec rake spec" 29 | end 30 | end 31 | end 32 | 33 | require 'rdoc/task' 34 | 35 | Rake::RDocTask.new do |rdoc| 36 | require 'kaminari/version' 37 | 38 | rdoc.rdoc_dir = 'rdoc' 39 | rdoc.title = "kaminari #{Kaminari::VERSION}" 40 | rdoc.rdoc_files.include('README*') 41 | rdoc.rdoc_files.include('lib/**/*.rb') 42 | end 43 | -------------------------------------------------------------------------------- /spec/support/matchers.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :contain_tag do |klass| 2 | match do |collection| 3 | if @num.blank? 4 | collection.any? {|tag| tag.is_a? klass} 5 | else 6 | (@count = collection.count {|tag| tag.is_a? klass}) == @num 7 | end 8 | end 9 | 10 | def count(num) 11 | @num = num 12 | self 13 | end 14 | 15 | description do 16 | "contain #{@num || 'any'} instance(s) of #{klass.name}" 17 | end 18 | failure_message_for_should do |collection| 19 | "expected #{@num || 'any'} instance(s) of #{klass.name} but was #{@count}" 20 | end 21 | end 22 | 23 | RSpec::Matchers.define :contain_tag_old do |count| 24 | match do |collection| 25 | (@count = collection.count {|tag| tag.is_a? @klass}) == count 26 | end 27 | 28 | def instance_of(klass) 29 | @klass = klass 30 | self 31 | end 32 | alias :instances_of :instance_of 33 | 34 | description do 35 | "contain #{count || 'any'} instance(s) of #{@klass.name}" 36 | end 37 | failure_message_for_should do |collection| 38 | "expected #{count || 'any'} instance(s) of #{@klass.name} but was #{@count}" 39 | end 40 | end 41 | 42 | RSpec::Matchers.define :skip do |num| 43 | match do |criteria| 44 | criteria.instance_variable_get('@options')[:skip] == num 45 | end 46 | end 47 | 48 | RSpec::Matchers.define :offset do |num| 49 | match do |collection| 50 | collection.offset_value == num 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /kaminari.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "kaminari/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'kaminari' 7 | s.version = Kaminari::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ['Akira Matsuda'] 10 | s.email = ['ronnie@dio.jp'] 11 | s.homepage = 'https://github.com/amatsuda/kaminari' 12 | s.summary = 'A pagination engine plugin for Rails 3 or other modern frameworks' 13 | s.description = 'Kaminari is a Scope & Engine based, clean, powerful, agnostic, customizable and sophisticated paginator for Rails 3' 14 | 15 | s.rubyforge_project = 'kaminari' 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.extra_rdoc_files = ['README.rdoc'] 20 | s.require_paths = ['lib'] 21 | 22 | s.licenses = ['MIT'] 23 | 24 | s.add_dependency 'activesupport', ['>= 3.0.0'] 25 | s.add_dependency 'actionpack', ['>= 3.0.0'] 26 | 27 | s.add_development_dependency 'bundler', ['>= 1.0.0'] 28 | s.add_development_dependency 'rake', ['>= 0'] 29 | s.add_development_dependency 'sqlite3', ['>= 0'] 30 | s.add_development_dependency 'tzinfo', ['>= 0'] 31 | s.add_development_dependency 'rspec', ['>= 0'] 32 | s.add_development_dependency 'rr', ['>= 0'] 33 | s.add_development_dependency 'capybara', ['>= 1.0'] 34 | s.add_development_dependency 'database_cleaner', ['>= 0'] 35 | s.add_development_dependency 'rdoc', ['>= 0'] 36 | end 37 | -------------------------------------------------------------------------------- /lib/kaminari/config.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/configurable' 2 | 3 | module Kaminari 4 | # Configures global settings for Kaminari 5 | # Kaminari.configure do |config| 6 | # config.default_per_page = 10 7 | # end 8 | def self.configure(&block) 9 | yield @config ||= Kaminari::Configuration.new 10 | end 11 | 12 | # Global settings for Kaminari 13 | def self.config 14 | @config 15 | end 16 | 17 | # need a Class for 3.0 18 | class Configuration #:nodoc: 19 | include ActiveSupport::Configurable 20 | config_accessor :default_per_page 21 | config_accessor :max_per_page 22 | config_accessor :window 23 | config_accessor :outer_window 24 | config_accessor :left 25 | config_accessor :right 26 | config_accessor :page_method_name 27 | 28 | def param_name 29 | config.param_name.respond_to?(:call) ? config.param_name.call : config.param_name 30 | end 31 | 32 | # define param_name writer (copied from AS::Configurable) 33 | writer, line = 'def param_name=(value); config.param_name = value; end', __LINE__ 34 | singleton_class.class_eval writer, __FILE__, line 35 | class_eval writer, __FILE__, line 36 | end 37 | 38 | # this is ugly. why can't we pass the default value to config_accessor...? 39 | configure do |config| 40 | config.default_per_page = 25 41 | config.max_per_page = nil 42 | config.window = 4 43 | config.outer_window = 0 44 | config.left = 0 45 | config.right = 0 46 | config.page_method_name = :page 47 | config.param_name = :page 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/kaminari/hooks.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | class Hooks 3 | def self.init 4 | ActiveSupport.on_load(:active_record) do 5 | require 'kaminari/models/active_record_extension' 6 | ::ActiveRecord::Base.send :include, Kaminari::ActiveRecordExtension 7 | end 8 | 9 | begin; require 'data_mapper'; rescue LoadError; end 10 | if defined? ::DataMapper 11 | require 'dm-aggregates' 12 | require 'kaminari/models/data_mapper_extension' 13 | ::DataMapper::Collection.send :include, Kaminari::DataMapperExtension::Collection 14 | ::DataMapper::Model.append_extensions Kaminari::DataMapperExtension::Model 15 | # ::DataMapper::Model.send :extend, Kaminari::DataMapperExtension::Model 16 | end 17 | 18 | begin; require 'mongoid'; rescue LoadError; end 19 | if defined? ::Mongoid 20 | require 'kaminari/models/mongoid_extension' 21 | ::Mongoid::Criteria.send :include, Kaminari::MongoidExtension::Criteria 22 | ::Mongoid::Document.send :include, Kaminari::MongoidExtension::Document 23 | end 24 | 25 | ActiveSupport.on_load(:mongo_mapper) do 26 | require 'kaminari/models/mongo_mapper_extension' 27 | ::MongoMapper::Document.send :include, Kaminari::MongoMapperExtension::Document 28 | ::Plucky::Query.send :include, Kaminari::PluckyCriteriaMethods 29 | end 30 | require 'kaminari/models/array_extension' 31 | 32 | ActiveSupport.on_load(:action_view) do 33 | ::ActionView::Base.send :include, Kaminari::ActionViewExtension 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/fake_app/rails_app.rb: -------------------------------------------------------------------------------- 1 | # require 'rails/all' 2 | require 'action_controller/railtie' 3 | require 'action_view/railtie' 4 | 5 | require 'fake_app/active_record/config' if defined? ActiveRecord 6 | require 'fake_app/data_mapper/config' if defined? DataMapper 7 | require 'fake_app/mongoid/config' if defined? Mongoid 8 | require 'fake_app/mongo_mapper/config' if defined? MongoMapper 9 | # config 10 | app = Class.new(Rails::Application) 11 | app.config.secret_token = '3b7cd727ee24e8444053437c36cc66c4' 12 | app.config.session_store :cookie_store, :key => '_myapp_session' 13 | app.config.active_support.deprecation = :log 14 | # Rais.root 15 | app.config.root = File.dirname(__FILE__) 16 | Rails.backtrace_cleaner.remove_silencers! 17 | app.initialize! 18 | 19 | # routes 20 | app.routes.draw do 21 | resources :users 22 | end 23 | 24 | #models 25 | require 'fake_app/active_record/models' if defined? ActiveRecord 26 | require 'fake_app/data_mapper/models' if defined? DataMapper 27 | require 'fake_app/mongoid/models' if defined? Mongoid 28 | require 'fake_app/mongo_mapper/models' if defined? MongoMapper 29 | 30 | # controllers 31 | class ApplicationController < ActionController::Base; end 32 | class UsersController < ApplicationController 33 | def index 34 | @users = User.page params[:page] 35 | render :inline => <<-ERB 36 | <%= @users.map(&:name).join("\n") %> 37 | <%= paginate @users %> 38 | ERB 39 | end 40 | end 41 | 42 | if defined? ActiveRecord 43 | class AddressesController < ApplicationController 44 | def index 45 | @addresses = User::Address.page params[:page] 46 | render :inline => <<-ERB 47 | <%= @addresses.map(&:street).join("\n") %> 48 | <%= paginate @addresses %> 49 | ERB 50 | end 51 | end 52 | end 53 | 54 | # helpers 55 | Object.const_set(:ApplicationHelper, Module.new) 56 | -------------------------------------------------------------------------------- /spec/config/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Kaminari::Configuration do 4 | subject { Kaminari.config } 5 | describe 'default_per_page' do 6 | context 'by default' do 7 | its(:default_per_page) { should == 25 } 8 | end 9 | context 'configured via config block' do 10 | before do 11 | Kaminari.configure {|c| c.default_per_page = 17} 12 | end 13 | its(:default_per_page) { should == 17 } 14 | after do 15 | Kaminari.configure {|c| c.default_per_page = 25} 16 | end 17 | end 18 | end 19 | 20 | describe 'max_per_page' do 21 | context 'by default' do 22 | its(:max_per_page) { should == nil } 23 | end 24 | context 'configure via config block' do 25 | before do 26 | Kaminari.configure {|c| c.max_per_page = 100} 27 | end 28 | its(:max_per_page) { should == 100 } 29 | after do 30 | Kaminari.configure {|c| c.max_per_page = nil} 31 | end 32 | end 33 | end 34 | 35 | describe 'window' do 36 | context 'by default' do 37 | its(:window) { should == 4 } 38 | end 39 | end 40 | 41 | describe 'outer_window' do 42 | context 'by default' do 43 | its(:outer_window) { should == 0 } 44 | end 45 | end 46 | 47 | describe 'left' do 48 | context 'by default' do 49 | its(:left) { should == 0 } 50 | end 51 | end 52 | 53 | describe 'right' do 54 | context 'by default' do 55 | its(:right) { should == 0 } 56 | end 57 | end 58 | 59 | describe 'param_name' do 60 | context 'by default' do 61 | its(:param_name) { should == :page } 62 | end 63 | 64 | context 'configured via config block' do 65 | before do 66 | Kaminari.configure {|c| c.param_name = lambda { :test } } 67 | end 68 | 69 | its(:param_name) { should == :test } 70 | 71 | after do 72 | Kaminari.configure {|c| c.param_name = :page } 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/fake_app/active_record/models.rb: -------------------------------------------------------------------------------- 1 | # models 2 | class User < ActiveRecord::Base 3 | has_many :authorships 4 | has_many :readerships 5 | has_many :books_authored, :through => :authorships, :source => :book 6 | has_many :books_read, :through => :readerships, :source => :book 7 | has_many :addresses, :class_name => 'User::Address' 8 | 9 | def readers 10 | User.joins(:books_read => :authors).where(:authors_books => {:id => self}) 11 | end 12 | 13 | scope :by_name, order(:name) 14 | scope :by_read_count, lambda { 15 | cols = if connection.adapter_name == "PostgreSQL" 16 | column_names.map { |column| %{"users"."#{column}"} }.join(", ") 17 | else 18 | '"users"."id"' 19 | end 20 | group(cols).select("count(readerships.id) AS read_count, #{cols}").order('read_count DESC') 21 | } 22 | end 23 | class Authorship < ActiveRecord::Base 24 | belongs_to :user 25 | belongs_to :book 26 | end 27 | class Readership < ActiveRecord::Base 28 | belongs_to :user 29 | belongs_to :book 30 | end 31 | class Book < ActiveRecord::Base 32 | has_many :authorships 33 | has_many :readerships 34 | has_many :authors, :through => :authorships, :source => :user 35 | has_many :readers, :through => :readerships, :source => :user 36 | end 37 | # a model that is a descendant of AR::Base but doesn't directly inherit AR::Base 38 | class Admin < User 39 | end 40 | # a model with namespace 41 | class User::Address < ActiveRecord::Base 42 | belongs_to :user 43 | end 44 | 45 | #migrations 46 | class CreateAllTables < ActiveRecord::Migration 47 | def self.up 48 | create_table(:gem_defined_models) { |t| t.string :name; t.integer :age } 49 | create_table(:users) {|t| t.string :name; t.integer :age} 50 | create_table(:books) {|t| t.string :title} 51 | create_table(:readerships) {|t| t.integer :user_id; t.integer :book_id } 52 | create_table(:authorships) {|t| t.integer :user_id; t.integer :book_id } 53 | create_table(:user_addresses) {|t| t.string :street; t.integer :user_id } 54 | end 55 | end 56 | ActiveRecord::Migration.verbose = false 57 | CreateAllTables.up 58 | -------------------------------------------------------------------------------- /lib/kaminari/models/array_extension.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module' 2 | module Kaminari 3 | # Kind of Array that can paginate 4 | class PaginatableArray < Array 5 | include Kaminari::ConfigurationMethods::ClassMethods 6 | 7 | attr_internal_accessor :limit_value, :offset_value 8 | 9 | # ==== Options 10 | # * :limit - limit 11 | # * :offset - offset 12 | # * :total_count - total_count 13 | def initialize(original_array = [], options = {}) 14 | @_original_array, @_limit_value, @_offset_value, @_total_count = original_array, (options[:limit] || default_per_page).to_i, options[:offset].to_i, options[:total_count] 15 | 16 | if options[:limit] && options[:offset] 17 | extend Kaminari::PageScopeMethods 18 | end 19 | 20 | if options[:total_count] 21 | super original_array 22 | else 23 | super(original_array[@_offset_value, @_limit_value] || []) 24 | end 25 | end 26 | 27 | # items at the specified "page" 28 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 29 | def #{Kaminari.config.page_method_name}(num = 1) 30 | offset(limit_value * ([num.to_i, 1].max - 1)) 31 | end 32 | RUBY 33 | 34 | # returns another chunk of the original array 35 | def limit(num) 36 | self.class.new @_original_array, :limit => num, :offset => @_offset_value, :total_count => @_total_count 37 | end 38 | 39 | # total item numbers of the original array 40 | def total_count 41 | @_total_count || @_original_array.count 42 | end 43 | 44 | # returns another chunk of the original array 45 | def offset(num) 46 | self.class.new @_original_array, :limit => @_limit_value, :offset => num, :total_count => @_total_count 47 | end 48 | end 49 | 50 | # Wrap an Array object to make it paginatable 51 | # ==== Options 52 | # * :limit - limit 53 | # * :offset - offset 54 | # * :total_count - total_count 55 | def self.paginate_array(array, options = {}) 56 | PaginatableArray.new array, options 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/models/active_record/active_record_relation_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? ActiveRecord 4 | describe Kaminari::ActiveRecordRelationMethods do 5 | describe '#total_count' do 6 | before do 7 | @author = User.create! :name => 'author' 8 | @author2 = User.create! :name => 'author2' 9 | @author3 = User.create! :name => 'author3' 10 | @books = 2.times.map {|i| @author.books_authored.create!(:title => "title%03d" % i) } 11 | @books2 = 3.times.map {|i| @author2.books_authored.create!(:title => "title%03d" % i) } 12 | @books3 = 4.times.map {|i| @author3.books_authored.create!(:title => "subject%03d" % i) } 13 | @readers = 4.times.map { User.create! :name => 'reader' } 14 | @books.each {|book| book.readers << @readers } 15 | end 16 | 17 | context "when the scope includes an order which references a generated column" do 18 | it "should successfully count the results" do 19 | @author.readers.by_read_count.page(1).total_count.should == @readers.size 20 | end 21 | end 22 | context "when the scope use conditions on includes" do 23 | it "should keep includes and successfully count the results" do 24 | # Only @author and @author2 have books titled with the title00x partern 25 | User.includes(:books_authored).where("books.title LIKE 'title00%'").page(1).total_count.should == 2 26 | end 27 | end 28 | context "when total_count receives options" do 29 | it "should return a distinct total count" do 30 | User.page(1).total_count(:name, :distinct => true).should == 4 31 | end 32 | end 33 | context "when count receives options" do 34 | it "should return a distinct set by column" do 35 | User.page(1).count(:name, :distinct => true).should == 4 36 | end 37 | end 38 | context "when the scope returns an ActiveSupport::OrderedHash" do 39 | it "should not throw exception by passing options to count" do 40 | lambda { 41 | @author.readers.by_read_count.page(1).total_count(:name, :distinct => true) 42 | }.should_not raise_exception 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/models/mongo_mapper/mongo_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? MongoMapper 4 | describe Kaminari::MongoMapperExtension do 5 | before(:each) do 6 | User.destroy_all 7 | 41.times { User.create!({:salary => 1}) } 8 | end 9 | 10 | describe '#page' do 11 | context 'page 1' do 12 | subject { User.page(1) } 13 | it { should be_a Plucky::Query } 14 | its(:current_page) { should == 1 } 15 | its(:limit_value) { should == 25 } 16 | its(:total_pages) { should == 2 } 17 | it { should skip(0) } 18 | end 19 | 20 | context 'page 2' do 21 | subject { User.page 2 } 22 | it { should be_a Plucky::Query } 23 | its(:current_page) { should == 2 } 24 | its(:limit_value) { should == 25 } 25 | its(:total_pages) { should == 2 } 26 | it { should skip 25 } 27 | end 28 | 29 | context 'page "foobar"' do 30 | subject { User.page 'foobar' } 31 | it { should be_a Plucky::Query } 32 | its(:current_page) { should == 1 } 33 | its(:limit_value) { should == 25 } 34 | its(:total_pages) { should == 2 } 35 | it { should skip 0 } 36 | end 37 | 38 | context 'with criteria before' do 39 | it "should have the proper criteria source" do 40 | User.where(:salary => 1).page(2).criteria.source.should == {:salary => 1} 41 | end 42 | 43 | subject { User.where(:salary => 1).page 2 } 44 | its(:current_page) { should == 2 } 45 | its(:limit_value) { should == 25 } 46 | its(:total_pages) { should == 2 } 47 | it { should skip 25 } 48 | end 49 | 50 | context 'with criteria after' do 51 | it "should have the proper criteria source" do 52 | User.where(:salary => 1).page(2).criteria.source.should == {:salary => 1} 53 | end 54 | 55 | subject { User.page(2).where(:salary => 1) } 56 | its(:current_page) { should == 2 } 57 | its(:limit_value) { should == 25 } 58 | its(:total_pages) { should == 2 } 59 | it { should skip 25 } 60 | end 61 | end 62 | 63 | describe '#per' do 64 | subject { User.page(2).per(10) } 65 | it { should be_a Plucky::Query } 66 | its(:current_page) { should == 2 } 67 | its(:limit_value) { should == 10 } 68 | its(:total_pages) { should == 5 } 69 | it { should skip 10 } 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/kaminari/helpers/tags.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module Helpers 3 | # A tag stands for an HTML tag inside the paginator. 4 | # Basically, a tag has its own partial template file, so every tag can be 5 | # rendered into String using its partial template. 6 | # 7 | # The template file should be placed in your app/views/kaminari/ directory 8 | # with underscored class name (besides the "Tag" class. Tag is an abstract 9 | # class, so _tag parital is not needed). 10 | # e.g.) PrevLink -> app/views/kaminari/_prev_link.html.erb 11 | # 12 | # When no matching template were found in your app, the engine's pre 13 | # installed template will be used. 14 | # e.g.) Paginator -> $GEM_HOME/kaminari-x.x.x/app/views/kaminari/_paginator.html.erb 15 | class Tag 16 | def initialize(template, options = {}) #:nodoc: 17 | @template, @options = template, options.dup 18 | @param_name = @options.delete(:param_name) 19 | @theme = @options[:theme] ? "#{@options.delete(:theme)}/" : '' 20 | @params = @options[:params] ? template.params.merge(@options.delete :params) : template.params 21 | end 22 | 23 | def to_s(locals = {}) #:nodoc: 24 | @template.render :partial => "kaminari/#{@theme}#{self.class.name.demodulize.underscore}", :locals => @options.merge(locals) 25 | end 26 | 27 | def page_url_for(page) 28 | @template.url_for @params.merge(@param_name => (page <= 1 ? nil : page)) 29 | end 30 | end 31 | 32 | # Tag that contains a link 33 | module Link 34 | # target page number 35 | def page 36 | raise 'Override page with the actual page value to be a Page.' 37 | end 38 | # the link's href 39 | def url 40 | page_url_for page 41 | end 42 | def to_s(locals = {}) #:nodoc: 43 | super locals.merge(:url => url) 44 | end 45 | end 46 | 47 | # A page 48 | class Page < Tag 49 | include Link 50 | # target page number 51 | def page 52 | @options[:page] 53 | end 54 | def to_s(locals = {}) #:nodoc: 55 | super locals.merge(:page => page) 56 | end 57 | end 58 | 59 | # Link with page number that appears at the leftmost 60 | class FirstPage < Tag 61 | include Link 62 | def page #:nodoc: 63 | 1 64 | end 65 | end 66 | 67 | # Link with page number that appears at the rightmost 68 | class LastPage < Tag 69 | include Link 70 | def page #:nodoc: 71 | @options[:total_pages] 72 | end 73 | end 74 | 75 | # The "previous" page of the current page 76 | class PrevPage < Tag 77 | include Link 78 | def page #:nodoc: 79 | @options[:current_page] - 1 80 | end 81 | end 82 | 83 | # The "next" page of the current page 84 | class NextPage < Tag 85 | include Link 86 | def page #:nodoc: 87 | @options[:current_page] + 1 88 | end 89 | end 90 | 91 | # Non-link tag that stands for skipped pages... 92 | class Gap < Tag 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/models/array_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Kaminari::PaginatableArray do 4 | it { should have(0).items } 5 | 6 | context 'specifying limit and offset when initializing' do 7 | subject { Kaminari::PaginatableArray.new((1..100).to_a, :limit => 10, :offset => 20) } 8 | its(:current_page) { should == 3 } 9 | end 10 | 11 | let(:array) { Kaminari::PaginatableArray.new((1..100).to_a) } 12 | describe '#page' do 13 | shared_examples_for 'the first page of array' do 14 | it { should have(25).users } 15 | its(:current_page) { should == 1 } 16 | its(:first) { should == 1 } 17 | end 18 | 19 | shared_examples_for 'blank array page' do 20 | it { should have(0).items } 21 | end 22 | 23 | context 'page 1' do 24 | subject { array.page 1 } 25 | it_should_behave_like 'the first page of array' 26 | end 27 | 28 | context 'page 2' do 29 | subject { array.page 2 } 30 | it { should have(25).users } 31 | its(:current_page) { should == 2 } 32 | its(:first) { should == 26 } 33 | end 34 | 35 | context 'page without an argument' do 36 | subject { array.page } 37 | it_should_behave_like 'the first page of array' 38 | end 39 | 40 | context 'page < 1' do 41 | subject { array.page 0 } 42 | it_should_behave_like 'the first page of array' 43 | end 44 | 45 | context 'page > max page' do 46 | subject { array.page 5 } 47 | it_should_behave_like 'blank array page' 48 | end 49 | end 50 | 51 | describe '#per' do 52 | context 'page 1 per 5' do 53 | subject { array.page(1).per(5) } 54 | it { should have(5).users } 55 | its(:first) { should == 1 } 56 | end 57 | end 58 | 59 | describe '#total_pages' do 60 | context 'per 25 (default)' do 61 | subject { array.page } 62 | its(:total_pages) { should == 4 } 63 | end 64 | 65 | context 'per 7' do 66 | subject { array.page(2).per(7) } 67 | its(:total_pages) { should == 15 } 68 | end 69 | 70 | context 'per 65536' do 71 | subject { array.page(50).per(65536) } 72 | its(:total_pages) { should == 1 } 73 | end 74 | 75 | context 'per 0 (using default)' do 76 | subject { array.page(50).per(0) } 77 | it { should == [] } 78 | end 79 | 80 | context 'per -1 (using default)' do 81 | subject { array.page(5).per(-1) } 82 | it { should == [] } 83 | end 84 | 85 | context 'per "String value that can not be converted into Number" (using default)' do 86 | subject { array.page(5).per('aho') } 87 | it { should == [] } 88 | end 89 | end 90 | 91 | describe '#current_page' do 92 | context 'page 1' do 93 | subject { array.page } 94 | its(:current_page) { should == 1 } 95 | end 96 | 97 | context 'page 2' do 98 | subject { array.page(2).per 3 } 99 | its(:current_page) { should == 2 } 100 | end 101 | end 102 | 103 | describe '#count' do 104 | context 'page 1' do 105 | subject { array.page } 106 | its(:count) { should == 25 } 107 | end 108 | 109 | context 'page 2' do 110 | subject { array.page 2 } 111 | its(:count) { should == 25 } 112 | end 113 | end 114 | 115 | context 'when setting total count explicitly' do 116 | subject { Kaminari::PaginatableArray.new((1..10).to_a, :total_count => 9999).page(5).per(10) } 117 | it { should have(10).items } 118 | its(:first) { should == 1 } 119 | its(:total_count) { should == 9999 } 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/models/mongoid/mongoid_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? Mongoid 4 | describe Kaminari::MongoidExtension do 5 | before(:each) do 6 | 41.times do 7 | User.create!({:salary => 1}) 8 | end 9 | end 10 | 11 | describe '#page' do 12 | 13 | context 'page 1' do 14 | subject { User.page 1 } 15 | it { should be_a Mongoid::Criteria } 16 | its(:current_page) { should == 1 } 17 | its(:limit_value) { should == 25 } 18 | its(:total_pages) { should == 2 } 19 | it { should skip(0) } 20 | end 21 | 22 | context 'page 2' do 23 | subject { User.page 2 } 24 | it { should be_a Mongoid::Criteria } 25 | its(:current_page) { should == 2 } 26 | its(:limit_value) { should == 25 } 27 | its(:total_pages) { should == 2 } 28 | it { should skip 25 } 29 | end 30 | 31 | context 'page "foobar"' do 32 | subject { User.page 'foobar' } 33 | it { should be_a Mongoid::Criteria } 34 | its(:current_page) { should == 1 } 35 | its(:limit_value) { should == 25 } 36 | its(:total_pages) { should == 2 } 37 | it { should skip 0 } 38 | end 39 | 40 | shared_examples 'complete valid pagination' do 41 | if Mongoid::VERSION =~ /^3/ 42 | its(:selector) { should == {'salary' => 1} } 43 | else 44 | its(:selector) { should == {:salary => 1} } 45 | end 46 | its(:current_page) { should == 2 } 47 | its(:limit_value) { should == 25 } 48 | its(:total_pages) { should == 2 } 49 | it { should skip 25 } 50 | end 51 | 52 | context 'with criteria before' do 53 | subject { User.where(:salary => 1).page 2 } 54 | it_should_behave_like 'complete valid pagination' 55 | end 56 | 57 | context 'with criteria after' do 58 | subject { User.page(2).where(:salary => 1) } 59 | it_should_behave_like 'complete valid pagination' 60 | end 61 | end 62 | 63 | describe '#per' do 64 | subject { User.page(2).per(10) } 65 | it { should be_a Mongoid::Criteria } 66 | its(:current_page) { should == 2 } 67 | its(:limit_value) { should == 10 } 68 | its(:total_pages) { should == 5 } 69 | it { should skip 10 } 70 | end 71 | 72 | describe '#page in embedded documents' do 73 | before do 74 | @mongo_developer = MongoMongoidExtensionDeveloper.new 75 | @mongo_developer.frameworks.new(:name => "rails", :language => "ruby") 76 | @mongo_developer.frameworks.new(:name => "merb", :language => "ruby") 77 | @mongo_developer.frameworks.new(:name => "sinatra", :language => "ruby") 78 | @mongo_developer.frameworks.new(:name => "cakephp", :language => "php") 79 | @mongo_developer.frameworks.new(:name => "tornado", :language => "python") 80 | end 81 | 82 | context 'page 1' do 83 | subject { @mongo_developer.frameworks.page(1).per(1) } 84 | it { should be_a Mongoid::Criteria } 85 | its(:total_count) { should == 5 } 86 | its(:limit_value) { should == 1 } 87 | its(:current_page) { should == 1 } 88 | its(:total_pages) { should == 5 } 89 | end 90 | 91 | context 'with criteria after' do 92 | subject { @mongo_developer.frameworks.page(1).per(2).where(:language => "ruby") } 93 | it { should be_a Mongoid::Criteria } 94 | its(:total_count) { should == 3 } 95 | its(:limit_value) { should == 2 } 96 | its(:current_page) { should == 1 } 97 | its(:total_pages) { should == 2 } 98 | end 99 | 100 | context 'with criteria before' do 101 | subject { @mongo_developer.frameworks.where(:language => "ruby").page(1).per(2) } 102 | it { should be_a Mongoid::Criteria } 103 | its(:total_count) { should == 3 } 104 | its(:limit_value) { should == 2 } 105 | its(:current_page) { should == 1 } 106 | its(:total_pages) { should == 2 } 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/generators/kaminari/views_generator.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | module Generators 3 | 4 | class ViewsGenerator < Rails::Generators::NamedBase 5 | source_root File.expand_path('../../../../app/views/kaminari', __FILE__) 6 | 7 | class_option :template_engine, :type => :string, :aliases => '-e', :desc => 'Template engine for the views. Available options are "erb", "haml", and "slim".' 8 | 9 | def self.banner #:nodoc: 10 | <<-BANNER.chomp 11 | rails g kaminari:views THEME [options] 12 | 13 | Copies all paginator partial templates to your application. 14 | You can choose a template THEME by specifying one from the list below: 15 | 16 | - default 17 | The default one. 18 | This one is used internally while you don't override the partials. 19 | #{themes.map {|t| " - #{t.name}\n#{t.description}"}.join("\n")} 20 | BANNER 21 | end 22 | 23 | desc '' 24 | def copy_or_fetch #:nodoc: 25 | return copy_default_views if file_name == 'default' 26 | 27 | themes = self.class.themes 28 | if theme = themes.detect {|t| t.name == file_name} 29 | download_templates theme 30 | else 31 | say %Q[no such theme: #{file_name}\n avaliable themes: #{themes.map(&:name).join ", "}] 32 | end 33 | end 34 | 35 | private 36 | def self.themes 37 | begin 38 | @themes ||= GitHubApiHelper.get_files_in_master.group_by {|fn, _| fn[0...(fn.index('/') || 0)]}.delete_if {|fn, _| fn.blank?}.map do |name, files| 39 | Theme.new name, files 40 | end 41 | rescue SocketError 42 | [] 43 | end 44 | end 45 | 46 | def download_templates(theme) 47 | theme.templates_for(template_engine).each do |template| 48 | say " downloading #{template.name} from kaminari_themes..." 49 | create_file template.name, GitHubApiHelper.get_content_for("#{theme.name}/#{template.name}") 50 | end 51 | end 52 | 53 | def copy_default_views 54 | filename_pattern = File.join self.class.source_root, "*.html.#{template_engine}" 55 | Dir.glob(filename_pattern).map {|f| File.basename f}.each do |f| 56 | copy_file f, "app/views/kaminari/#{f}" 57 | end 58 | end 59 | 60 | def template_engine 61 | options[:template_engine].try(:to_s).try(:downcase) || 'erb' 62 | end 63 | end 64 | 65 | Template = Struct.new(:name, :sha) do 66 | def description? 67 | name == 'DESCRIPTION' 68 | end 69 | 70 | def view? 71 | name =~ /^app\/views\// 72 | end 73 | 74 | def engine #:nodoc: 75 | File.extname(name).sub /^\./, '' 76 | end 77 | end 78 | 79 | class Theme 80 | attr_accessor :name 81 | def initialize(name, templates) #:nodoc: 82 | @name, @templates = name, templates.map {|fn, sha| Template.new fn.sub(/^#{name}\//, ''), sha} 83 | end 84 | 85 | def description #:nodoc: 86 | file = @templates.detect(&:description?) 87 | return "#{' ' * 12}#{name}" unless file 88 | GitHubApiHelper.get_content_for("#{@name}/#{file.name}").chomp.gsub(/^/, ' ' * 12) 89 | end 90 | 91 | def templates_for(template_engine) #:nodoc: 92 | @templates.select {|t| !t.description?}.select {|t| !t.view? || (t.engine == template_engine)} 93 | end 94 | end 95 | 96 | module GitHubApiHelper 97 | def get_files_in_master 98 | master_tree_sha = open('https://api.github.com/repos/amatsuda/kaminari_themes/git/refs/heads/master') do |json| 99 | ActiveSupport::JSON.decode(json)['object']['sha'] 100 | end 101 | open('https://api.github.com/repos/amatsuda/kaminari_themes/git/trees/' + master_tree_sha + '?recursive=1') do |json| 102 | blobs = ActiveSupport::JSON.decode(json)['tree'].find_all {|i| i['type'] == 'blob' } 103 | blobs.map do |blob| 104 | [blob['path'], blob['sha']] 105 | end 106 | end 107 | end 108 | module_function :get_files_in_master 109 | 110 | def get_content_for(path) 111 | open('https://api.github.com/repos/amatsuda/kaminari_themes/contents/' + path) do |json| 112 | Base64.decode64(ActiveSupport::JSON.decode(json)['content']) 113 | end 114 | end 115 | module_function :get_content_for 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/kaminari/helpers/sinatra_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/object' 2 | require 'active_support/core_ext/string' 3 | 4 | begin 5 | 6 | require 'padrino-helpers' 7 | module Kaminari::Helpers 8 | module SinatraHelpers 9 | class << self 10 | def registered(app) 11 | app.register Padrino::Helpers 12 | app.helpers HelperMethods 13 | end 14 | 15 | alias included registered 16 | end 17 | 18 | class ActionViewTemplateProxy 19 | include Padrino::Helpers::OutputHelpers 20 | include Padrino::Helpers::TagHelpers 21 | include Padrino::Helpers::AssetTagHelpers 22 | include Padrino::Helpers::FormatHelpers 23 | include Padrino::Helpers::TranslationHelpers 24 | 25 | def initialize(opts={}) 26 | @current_path = opts[:current_path] 27 | @param_name = (opts[:param_name] || :page).to_sym 28 | @current_params = opts[:current_params] 29 | @current_params.delete(@param_name) 30 | end 31 | 32 | def render(*args) 33 | base = ActionView::Base.new.tap do |a| 34 | a.view_paths << File.expand_path('../../../../app/views', __FILE__) 35 | end 36 | base.render(*args) 37 | end 38 | 39 | def url_for(params) 40 | extra_params = {} 41 | if page = params[@param_name] and page != 1 42 | extra_params[@param_name] = page 43 | end 44 | query = @current_params.merge(extra_params) 45 | @current_path + (query.empty? ? '' : "?#{query.to_query}") 46 | end 47 | 48 | def link_to_unless(condition, name, options = {}, html_options = {}, &block) 49 | options = url_for(options) if options.is_a? Hash 50 | if condition 51 | if block_given? 52 | block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block) 53 | else 54 | name 55 | end 56 | else 57 | link_to(name, options, html_options) 58 | end 59 | end 60 | 61 | def params 62 | @current_params 63 | end 64 | end 65 | 66 | module HelperMethods 67 | # A helper that renders the pagination links - for Sinatra. 68 | # 69 | # <%= paginate @articles %> 70 | # 71 | # ==== Options 72 | # * :window - The "inner window" size (4 by default). 73 | # * :outer_window - The "outer window" size (0 by default). 74 | # * :left - The "left outer window" size (0 by default). 75 | # * :right - The "right outer window" size (0 by default). 76 | # * :params - url_for parameters for the links (:id, :locale, etc.) 77 | # * :param_name - parameter name for page number in the links (:page by default) 78 | # * :remote - Ajax? (false by default) 79 | # * :ANY_OTHER_VALUES - Any other hash key & values would be directly passed into each tag as :locals value. 80 | def paginate(scope, options = {}, &block) 81 | current_path = env['PATH_INFO'] rescue nil 82 | current_params = Rack::Utils.parse_query(env['QUERY_STRING']).symbolize_keys rescue {} 83 | paginator = Kaminari::Helpers::Paginator.new( 84 | ActionViewTemplateProxy.new(:current_params => current_params, :current_path => current_path, :param_name => options[:param_name] || Kaminari.config.param_name), 85 | options.reverse_merge(:current_page => scope.current_page, :total_pages => scope.total_pages, :per_page => scope.limit_value, :param_name => Kaminari.config.param_name, :remote => false) 86 | ) 87 | paginator.to_s 88 | end 89 | 90 | # A simple "Twitter like" pagination link that creates a link to the next page. 91 | # Works on Sinatra. 92 | # 93 | # ==== Examples 94 | # Basic usage: 95 | # 96 | # <%= link_to_next_page @items, 'Next Page' %> 97 | # 98 | # Ajax: 99 | # 100 | # <%= link_to_next_page @items, 'Next Page', :remote => true %> 101 | # 102 | # By default, it renders nothing if there are no more results on the next page. 103 | # You can customize this output by passing a parameter :placeholder. 104 | # 105 | # <%= link_to_next_page @items, 'Next Page', :placeholder => %{No More Pages} %> 106 | # 107 | def link_to_next_page(scope, name, options = {}) 108 | params = options.delete(:params) || (Rack::Utils.parse_query(env['QUERY_STRING']).symbolize_keys rescue {}) 109 | param_name = options.delete(:param_name) || Kaminari.config.param_name 110 | placeholder = options.delete(:placeholder) 111 | query = params.merge(param_name => (scope.current_page + 1)) 112 | unless scope.last_page? 113 | link_to name, env['PATH_INFO'] + (query.empty? ? '' : "?#{query.to_query}"), options.reverse_merge(:rel => 'next') 114 | else 115 | placeholder 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | if defined? I18n 123 | I18n.load_path += Dir.glob(File.expand_path('../../../../config/locales/*.yml', __FILE__)) 124 | end 125 | 126 | rescue LoadError 127 | 128 | $stderr.puts "[!]You shold install `padrino-helpers' gem if you want to use kaminari's pagination helpers with Sinatra." 129 | $stderr.puts "[!]Kaminari::Helpers::SinatraHelper does nothing now..." 130 | 131 | module Kaminari::Helpers 132 | module SinatraHelper 133 | def self.registered(*) 134 | end 135 | end 136 | end 137 | 138 | end 139 | -------------------------------------------------------------------------------- /spec/helpers/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | include Kaminari::Helpers 3 | 4 | describe 'Kaminari::Helpers::Paginator' do 5 | let :template do 6 | stub(r = Object.new) do 7 | render.with_any_args 8 | params { {} } 9 | options { {} } 10 | url_for {|h| "/foo?page=#{h[:page]}"} 11 | end 12 | r 13 | end 14 | 15 | describe '#params' do 16 | before do 17 | @paginator = Paginator.new(template, :params => {:controller => 'foo', :action => 'bar'}) 18 | end 19 | subject { @paginator.page_tag(template).instance_variable_get('@params') } 20 | it { should == {:controller => 'foo', :action => 'bar'} } 21 | end 22 | 23 | describe '#param_name' do 24 | before do 25 | @paginator = Paginator.new(template, :param_name => :pagina) 26 | end 27 | subject { @paginator.page_tag(template).instance_variable_get('@param_name') } 28 | it { should == :pagina } 29 | end 30 | 31 | #TODO test somehow... 32 | # describe '#tagify_links' do 33 | # def tags_with(options) 34 | # PaginationRenderer.new(template, options).tagify_links 35 | # end 36 | 37 | # context '1 page in total' do 38 | # subject { tags_with :total_pages => 1, :current_page => 1 } 39 | # it { should have(0).tags } 40 | # end 41 | 42 | # context '10 pages in total' do 43 | # context 'first page' do 44 | # subject { tags_with :total_pages => 10, :current_page => 1 } 45 | # it { should_not contain_tag PrevLink } 46 | # it { should contain_tag PrevSpan } 47 | # it { should contain_tag CurrentPage } 48 | # it { should_not contain_tag FirstPageLink } 49 | # it { should contain_tag LastPageLink } 50 | # it { should contain_tag PageLink } 51 | # it { should contain_tag NextLink } 52 | # it { should_not contain_tag NextSpan } 53 | # it { should contain_tag TruncatedSpan } 54 | # end 55 | 56 | # context 'second page' do 57 | # subject { tags_with :total_pages => 10, :current_page => 2 } 58 | # it { should contain_tag PrevLink } 59 | # it { should_not contain_tag PrevSpan } 60 | # it { should contain_tag CurrentPage } 61 | # it { should contain_tag FirstPageLink } 62 | # it { should contain_tag LastPageLink } 63 | # it { should contain_tag PageLink } 64 | # it { should contain_tag NextLink } 65 | # it { should_not contain_tag NextSpan } 66 | # it { should contain_tag TruncatedSpan } 67 | # end 68 | 69 | # context 'third page' do 70 | # subject { tags_with :total_pages => 10, :current_page => 3 } 71 | # it { should contain_tag PrevLink } 72 | # it { should_not contain_tag PrevSpan } 73 | # it { should contain_tag CurrentPage } 74 | # it { should contain_tag FirstPageLink } 75 | # it { should contain_tag LastPageLink } 76 | # it { should contain_tag PageLink } 77 | # it { should contain_tag NextLink } 78 | # it { should_not contain_tag NextSpan } 79 | # it { should contain_tag TruncatedSpan } 80 | # end 81 | 82 | # context 'fourth page(no truncation)' do 83 | # subject { tags_with :total_pages => 10, :current_page => 4 } 84 | # it { should contain_tag PrevLink } 85 | # it { should_not contain_tag PrevSpan } 86 | # it { should contain_tag CurrentPage } 87 | # it { should contain_tag FirstPageLink } 88 | # it { should contain_tag LastPageLink } 89 | # it { should contain_tag PageLink } 90 | # it { should contain_tag NextLink } 91 | # it { should_not contain_tag NextSpan } 92 | # it { should_not contain_tag TruncatedSpan } 93 | # end 94 | 95 | # context 'seventh page(no truncation)' do 96 | # subject { tags_with :total_pages => 10, :current_page => 7 } 97 | # it { should contain_tag PrevLink } 98 | # it { should_not contain_tag PrevSpan } 99 | # it { should contain_tag CurrentPage } 100 | # it { should contain_tag FirstPageLink } 101 | # it { should contain_tag LastPageLink } 102 | # it { should contain_tag PageLink } 103 | # it { should contain_tag NextLink } 104 | # it { should_not contain_tag NextSpan } 105 | # it { should_not contain_tag TruncatedSpan } 106 | # end 107 | 108 | # context 'eighth page' do 109 | # subject { tags_with :total_pages => 10, :current_page => 8 } 110 | # it { should contain_tag PrevLink } 111 | # it { should_not contain_tag PrevSpan } 112 | # it { should contain_tag CurrentPage } 113 | # it { should contain_tag FirstPageLink } 114 | # it { should contain_tag LastPageLink } 115 | # it { should contain_tag PageLink } 116 | # it { should contain_tag NextLink } 117 | # it { should_not contain_tag NextSpan } 118 | # it { should contain_tag TruncatedSpan } 119 | # end 120 | 121 | # context 'last page' do 122 | # subject { tags_with :total_pages => 10, :current_page => 10 } 123 | # it { should contain_tag PrevLink } 124 | # it { should_not contain_tag PrevSpan } 125 | # it { should contain_tag CurrentPage } 126 | # it { should contain_tag FirstPageLink } 127 | # it { should_not contain_tag LastPageLink } 128 | # it { should contain_tag PageLink } 129 | # it { should_not contain_tag NextLink } 130 | # it { should contain_tag NextSpan } 131 | # it { should contain_tag TruncatedSpan } 132 | # end 133 | # end 134 | # end 135 | end 136 | -------------------------------------------------------------------------------- /spec/models/active_record/scopes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? ActiveRecord 4 | 5 | shared_examples_for 'the first page' do 6 | it { should have(25).users } 7 | its('first.name') { should == 'user001' } 8 | end 9 | 10 | shared_examples_for 'blank page' do 11 | it { should have(0).users } 12 | end 13 | 14 | describe Kaminari::ActiveRecordExtension do 15 | before do 16 | 1.upto(100) {|i| User.create! :name => "user#{'%03d' % i}", :age => (i / 10)} 17 | 1.upto(100) {|i| GemDefinedModel.create! :name => "user#{'%03d' % i}", :age => (i / 10)} 18 | end 19 | 20 | [User, Admin, GemDefinedModel].each do |model_class| 21 | context "for #{model_class}" do 22 | describe '#page' do 23 | context 'page 1' do 24 | subject { model_class.page 1 } 25 | it_should_behave_like 'the first page' 26 | end 27 | 28 | context 'page 2' do 29 | subject { model_class.page 2 } 30 | it { should have(25).users } 31 | its('first.name') { should == 'user026' } 32 | end 33 | 34 | context 'page without an argument' do 35 | subject { model_class.page } 36 | it_should_behave_like 'the first page' 37 | end 38 | 39 | context 'page < 1' do 40 | subject { model_class.page 0 } 41 | it_should_behave_like 'the first page' 42 | end 43 | 44 | context 'page > max page' do 45 | subject { model_class.page 5 } 46 | it_should_behave_like 'blank page' 47 | end 48 | 49 | describe 'ensure #order_values is preserved' do 50 | subject { model_class.order('id').page 1 } 51 | its('order_values.uniq') { should == ['id'] } 52 | end 53 | end 54 | 55 | describe '#per' do 56 | context 'page 1 per 5' do 57 | subject { model_class.page(1).per(5) } 58 | it { should have(5).users } 59 | its('first.name') { should == 'user001' } 60 | end 61 | end 62 | 63 | describe '#padding' do 64 | context 'page 1 per 5 padding 1' do 65 | subject { model_class.page(1).per(5).padding(1) } 66 | it { should have(5).users } 67 | its('first.name') { should == 'user002' } 68 | end 69 | 70 | context 'page 1 per 0 padding 1' do 71 | subject { model_class.page(1).per(0).padding(1) } 72 | it { should == [] } 73 | end 74 | end 75 | 76 | describe '#total_pages' do 77 | context 'per 25 (default)' do 78 | subject { model_class.page } 79 | its(:total_pages) { should == 4 } 80 | end 81 | 82 | context 'per 7' do 83 | subject { model_class.page(2).per(7) } 84 | its(:total_pages) { should == 15 } 85 | end 86 | 87 | context 'per 65536' do 88 | subject { model_class.page(50).per(65536) } 89 | its(:total_pages) { should == 1 } 90 | end 91 | 92 | context 'per 0 (using default)' do 93 | subject { model_class.page(50).per(0) } 94 | it { should == [] } 95 | end 96 | 97 | context 'per -1 (using default)' do 98 | subject { model_class.page(5).per(-1) } 99 | it { should == [] } 100 | end 101 | 102 | context 'per "String value that can not be converted into Number" (using default)' do 103 | subject { model_class.page(5).per('aho') } 104 | it { should == [] } 105 | end 106 | end 107 | 108 | 109 | describe '#current_page' do 110 | context 'page 1' do 111 | subject { model_class.page } 112 | its(:current_page) { should == 1 } 113 | end 114 | 115 | context 'page 2' do 116 | subject { model_class.page(2).per 3 } 117 | its(:current_page) { should == 2 } 118 | end 119 | end 120 | 121 | describe '#first_page?' do 122 | context 'on first page' do 123 | subject { model_class.page(1).per(10) } 124 | its(:first_page?) { should == true } 125 | end 126 | 127 | context 'not on first page' do 128 | subject { model_class.page(5).per(10) } 129 | its(:first_page?) { should == false } 130 | end 131 | end 132 | 133 | describe '#last_page?' do 134 | context 'on last page' do 135 | subject { model_class.page(10).per(10) } 136 | its(:last_page?) { should == true } 137 | end 138 | 139 | context 'not on last page' do 140 | subject { model_class.page(1).per(10) } 141 | its(:last_page?) { should == false } 142 | end 143 | end 144 | 145 | describe '#count' do 146 | context 'page 1' do 147 | subject { model_class.page } 148 | its(:count) { should == 25 } 149 | end 150 | 151 | context 'page 2' do 152 | subject { model_class.page 2 } 153 | its(:count) { should == 25 } 154 | end 155 | end 156 | 157 | context 'chained with .group' do 158 | subject { model_class.group('age').page(2).per 5 } 159 | # 0..10 160 | its(:total_count) { should == 11 } 161 | its(:total_pages) { should == 3 } 162 | end 163 | 164 | context 'activerecord descendants' do 165 | subject { ActiveRecord::Base.descendants } 166 | its(:length) { should_not == 0 } 167 | end 168 | end 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /spec/helpers/sinatra_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? Sinatra 4 | ERB_TEMPLATE_FOR_PAGINATE = < 6 | 11 | <%= paginate @users, @options %> 12 | 13 | EOT 14 | 15 | ERB_TEMPLATE_FOR_NEXT_PAGE = < 17 |
    18 | <% @users.each do |user| %> 19 | 20 | <% end %> 21 |
22 | <%= link_to_next_page(@users, "Next!", {:id => 'next_page_link'}.merge(@options || {})) %> 23 | 24 | EOT 25 | 26 | describe 'Kaminari::Helpers::SinatraHelper' do 27 | before do 28 | 50.times {|i| User.create! :name => "user#{i}"} 29 | end 30 | 31 | describe '#paginate' do 32 | before do 33 | mock_app do 34 | register Kaminari::Helpers::SinatraHelpers 35 | get '/users' do 36 | @page = params[:page] || 1 37 | @users = User.page(@page) 38 | @options = {} 39 | erb ERB_TEMPLATE_FOR_PAGINATE 40 | end 41 | end 42 | end 43 | 44 | context 'normal paginations with Sinatra' do 45 | before { get '/users' } 46 | 47 | it 'should have a navigation tag' do 48 | last_document.search('nav.pagination').should_not be_empty 49 | end 50 | 51 | it 'should have pagination links' do 52 | last_document.search('.page a').should have_at_least(1).items 53 | last_document.search('.next a').should have_at_least(1).items 54 | last_document.search('.last a').should have_at_least(1).items 55 | end 56 | 57 | it 'should point to current page' do 58 | last_document.search('.current').text.should match(/1/) 59 | 60 | get '/users?page=2' 61 | last_document.search('.current').text.should match(/2/) 62 | end 63 | 64 | it 'should load 25 users' do 65 | last_document.search('li.user_info').should have(25).items 66 | end 67 | 68 | it 'should preserve params' do 69 | get '/users?foo=bar' 70 | last_document.search('.page a').should(be_all do |elm| 71 | elm.attribute('href').value =~ /foo=bar/ 72 | end) 73 | end 74 | end 75 | 76 | context 'optional paginations with Sinatra' do 77 | it 'should have 5 windows with 1 gap' do 78 | mock_app do 79 | register Kaminari::Helpers::SinatraHelpers 80 | get '/users' do 81 | @page = params[:page] || 1 82 | @users = User.page(@page).per(5) 83 | @options = {} 84 | erb ERB_TEMPLATE_FOR_PAGINATE 85 | end 86 | end 87 | 88 | get '/users' 89 | last_document.search('.page').should have(6).items 90 | last_document.search('.gap').should have(1).item 91 | end 92 | 93 | it 'should controll the inner window size' do 94 | mock_app do 95 | register Kaminari::Helpers::SinatraHelpers 96 | get '/users' do 97 | @page = params[:page] || 1 98 | @users = User.page(@page).per(3) 99 | @options = {:window => 10} 100 | erb ERB_TEMPLATE_FOR_PAGINATE 101 | end 102 | end 103 | 104 | get '/users' 105 | last_document.search('.page').should have(12).items 106 | last_document.search('.gap').should have(1).item 107 | end 108 | 109 | it 'should specify a page param name' do 110 | mock_app do 111 | register Kaminari::Helpers::SinatraHelpers 112 | get '/users' do 113 | @page = params[:page] || 1 114 | @users = User.page(@page).per(3) 115 | @options = {:param_name => :user_page} 116 | erb ERB_TEMPLATE_FOR_PAGINATE 117 | end 118 | end 119 | 120 | get '/users' 121 | last_document.search('.page a').should(be_all do |elm| 122 | elm.attribute('href').value =~ /user_page=\d+/ 123 | end) 124 | end 125 | end 126 | end 127 | 128 | describe '#link_to_next_page' do 129 | before do 130 | mock_app do 131 | register Kaminari::Helpers::SinatraHelpers 132 | get '/users' do 133 | @page = params[:page] || 1 134 | @users = User.page(@page) 135 | erb ERB_TEMPLATE_FOR_NEXT_PAGE 136 | end 137 | 138 | get '/users_placeholder' do 139 | @page = params[:page] || 1 140 | @options = {:placeholder => %{No Next Page}} 141 | @users = User.page(@page) 142 | erb ERB_TEMPLATE_FOR_NEXT_PAGE 143 | end 144 | end 145 | end 146 | 147 | context 'having more page' do 148 | it 'should have a more page link' do 149 | get '/users' 150 | last_document.search('a#next_page_link').should be_present 151 | last_document.search('a#next_page_link').text.should match(/Next!/) 152 | end 153 | end 154 | 155 | context 'the last page' do 156 | it 'should not have a more page link' do 157 | get '/users?page=2' 158 | last_document.search('a#next_page_link').should be_empty 159 | end 160 | 161 | it 'should have a no more page notation using placeholder' do 162 | get '/users_placeholder?page=2' 163 | last_document.search('a#next_page_link').should be_empty 164 | last_document.search('span#no_next_page').should be_present 165 | last_document.search('span#no_next_page').text.should match(/No Next Page/) 166 | end 167 | end 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /spec/helpers/tags_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | include Kaminari::Helpers 3 | 4 | describe 'Kaminari::Helpers' do 5 | describe 'Paginator' do 6 | describe 'Paginator::PageProxy' do 7 | describe '#current?' do 8 | context 'current_page == page' do 9 | subject { Paginator::PageProxy.new({:current_page => 26}, 26, nil) } 10 | its(:current?) { should be_true } 11 | end 12 | context 'current_page != page' do 13 | subject { Paginator::PageProxy.new({:current_page => 13}, 26, nil) } 14 | its(:current?) { should_not be_true } 15 | end 16 | end 17 | 18 | describe '#first?' do 19 | context 'page == 1' do 20 | subject { Paginator::PageProxy.new({:current_page => 26}, 1, nil) } 21 | its(:first?) { should be_true } 22 | end 23 | context 'page != 1' do 24 | subject { Paginator::PageProxy.new({:current_page => 13}, 2, nil) } 25 | its(:first?) { should_not be_true } 26 | end 27 | end 28 | 29 | describe '#last?' do 30 | context 'current_page == page' do 31 | subject { Paginator::PageProxy.new({:total_pages => 39}, 39, nil) } 32 | its(:last?) { should be_true } 33 | end 34 | context 'current_page != page' do 35 | subject { Paginator::PageProxy.new({:total_pages => 39}, 38, nil) } 36 | its(:last?) { should_not be_true } 37 | end 38 | end 39 | 40 | describe '#next?' do 41 | context 'page == current_page + 1' do 42 | subject { Paginator::PageProxy.new({:current_page => 52}, 53, nil) } 43 | its(:next?) { should be_true } 44 | end 45 | context 'page != current_page + 1' do 46 | subject { Paginator::PageProxy.new({:current_page => 52}, 77, nil) } 47 | its(:next?) { should_not be_true } 48 | end 49 | end 50 | 51 | describe '#prev?' do 52 | context 'page == current_page - 1' do 53 | subject { Paginator::PageProxy.new({:current_page => 77}, 76, nil) } 54 | its(:prev?) { should be_true } 55 | end 56 | context 'page != current_page + 1' do 57 | subject { Paginator::PageProxy.new({:current_page => 77}, 80, nil) } 58 | its(:prev?) { should_not be_true } 59 | end 60 | end 61 | 62 | describe '#left_outer?' do 63 | context 'current_page == left' do 64 | subject { Paginator::PageProxy.new({:left => 3}, 3, nil) } 65 | its(:left_outer?) { should be_true } 66 | end 67 | context 'current_page == left + 1' do 68 | subject { Paginator::PageProxy.new({:left => 3}, 4, nil) } 69 | its(:left_outer?) { should_not be_true } 70 | end 71 | context 'current_page == left + 2' do 72 | subject { Paginator::PageProxy.new({:left => 3}, 5, nil) } 73 | its(:left_outer?) { should_not be_true } 74 | end 75 | end 76 | 77 | describe '#right_outer?' do 78 | context 'total_pages - page > right' do 79 | subject { Paginator::PageProxy.new({:total_pages => 10, :right => 3}, 6, nil) } 80 | its(:right_outer?) { should_not be_true } 81 | end 82 | context 'total_pages - page == right' do 83 | subject { Paginator::PageProxy.new({:total_pages => 10, :right => 3}, 7, nil) } 84 | its(:right_outer?) { should_not be_true } 85 | end 86 | context 'total_pages - page < right' do 87 | subject { Paginator::PageProxy.new({:total_pages => 10, :right => 3}, 8, nil) } 88 | its(:right_outer?) { should be_true } 89 | end 90 | end 91 | 92 | describe '#inside_window?' do 93 | context 'page > current_page' do 94 | context 'page - current_page > window' do 95 | subject { Paginator::PageProxy.new({:current_page => 4, :window => 5}, 10, nil) } 96 | its(:inside_window?) { should_not be_true } 97 | end 98 | context 'page - current_page == window' do 99 | subject { Paginator::PageProxy.new({:current_page => 4, :window => 6}, 10, nil) } 100 | its(:inside_window?) { should be_true } 101 | end 102 | context 'page - current_page < window' do 103 | subject { Paginator::PageProxy.new({:current_page => 4, :window => 7}, 10, nil) } 104 | its(:inside_window?) { should be_true } 105 | end 106 | end 107 | context 'current_page > page' do 108 | context 'current_page - page > window' do 109 | subject { Paginator::PageProxy.new({:current_page => 15, :window => 4}, 10, nil) } 110 | its(:inside_window?) { should_not be_true } 111 | end 112 | context 'current_page - page == window' do 113 | subject { Paginator::PageProxy.new({:current_page => 15, :window => 5}, 10, nil) } 114 | its(:inside_window?) { should be_true } 115 | end 116 | context 'current_page - page < window' do 117 | subject { Paginator::PageProxy.new({:current_page => 15, :window => 6}, 10, nil) } 118 | its(:inside_window?) { should be_true } 119 | end 120 | end 121 | end 122 | describe '#was_truncated?' do 123 | before do 124 | stub(@template = Object.new) do 125 | options { {} } 126 | params { {} } 127 | end 128 | end 129 | context 'last.is_a? Gap' do 130 | subject { Paginator::PageProxy.new({}, 10, Gap.new(@template)) } 131 | its(:was_truncated?) { should be_true } 132 | end 133 | context 'last.is not a Gap' do 134 | subject { Paginator::PageProxy.new({}, 10, Page.new(@template)) } 135 | its(:was_truncated?) { should_not be_true } 136 | end 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /spec/models/data_mapper/data_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if defined? DataMapper 4 | describe Kaminari::DataMapperExtension do 5 | before do 6 | 100.times do |i| 7 | User.create(:name => "User#{i}", :age => i) 8 | end 9 | 10 | worker0 = User[0] 11 | 50.times do |i| 12 | worker0.projects << Project.create(:name => "Project#{i}") 13 | end 14 | worker0.projects.save 15 | end 16 | 17 | describe 'Collection' do 18 | subject{ User.all } 19 | it { should respond_to(:page) } 20 | it { should_not respond_to(:per) } 21 | end 22 | 23 | describe 'Model' do 24 | subject{ User } 25 | it { should respond_to(:page) } 26 | it { should respond_to(:default_per_page) } 27 | it { should_not respond_to(:per) } 28 | end 29 | 30 | describe '#page' do 31 | context 'page 0' do 32 | subject { User.all(:age.gte => 60).page 0 } 33 | it { should be_a DataMapper::Collection } 34 | its(:current_page) { should == 1 } 35 | its('query.limit') { should == 25 } 36 | its('query.offset') { should == 0 } 37 | its(:total_count) { should == User.count(:age.gte => 60) } 38 | its(:total_pages) { should == 2 } 39 | end 40 | 41 | context 'page 1' do 42 | subject { User.all(:age.gte => 0).page 1 } 43 | it { should be_a DataMapper::Collection } 44 | its(:current_page) { should == 1 } 45 | its('query.limit') { should == 25 } 46 | its('query.offset') { should == 0 } 47 | its(:total_count) { should == 100 } 48 | its(:total_pages) { should == 4 } 49 | end 50 | 51 | context 'page 2' do 52 | subject { User.page 2 } 53 | it { should be_a DataMapper::Collection } 54 | its(:current_page) { should == 2 } 55 | its(:limit_value) { should == 25 } 56 | its('query.limit') { should == 25 } 57 | its('query.offset') { should == 25 } 58 | its(:total_count) { should == 100 } 59 | its(:total_pages) { should == 4 } 60 | end 61 | 62 | context 'page "foobar"' do 63 | subject { User.page 'foobar' } 64 | it { should be_a DataMapper::Collection } 65 | its(:current_page) { should == 1 } 66 | its('query.limit') { should == 25 } 67 | its('query.offset') { should == 0 } 68 | its(:total_count) { should == 100 } 69 | its(:total_pages) { should == 4 } 70 | end 71 | 72 | context 'with criteria before' do 73 | subject { User.all(:age.gt => 60).page 2 } 74 | it { should be_a DataMapper::Collection } 75 | its(:current_page) { should == 2 } 76 | its('query.limit') { should == 25 } 77 | its('query.offset') { should == 25 } 78 | its(:total_count) { should == User.count(:age.gt => 60) } 79 | its(:total_pages) { should == 2 } 80 | end 81 | 82 | context 'with criteria after' do 83 | subject { User.page(2).all(:age.gt => 60) } 84 | it { should be_a DataMapper::Collection } 85 | its(:current_page) { should == 2 } 86 | its('query.limit') { should == 25 } 87 | its('query.offset') { should == 25 } 88 | its(:total_count) { should == User.count(:age.gt => 60) } 89 | its(:total_pages) { should == 2 } 90 | end 91 | end 92 | 93 | describe '#per' do 94 | context 'on simple query' do 95 | subject { User.page(2).per(20) } 96 | it { should be_a DataMapper::Collection } 97 | its(:current_page) { should == 2 } 98 | its('query.limit') { should == 20 } 99 | its(:limit_value) { should == 20 } 100 | its('query.offset') { should == 20 } 101 | its(:total_count) { should == 100 } 102 | its(:total_pages) { should == 5 } 103 | end 104 | 105 | context 'on query with condition' do 106 | subject { User.page(5).all(:age.lte => 80).per(13) } 107 | its(:current_page) { should == 5 } 108 | its('query.limit') { should == 13 } 109 | its('query.offset') { should == 52 } 110 | its(:total_count) { should == 81 } 111 | its(:total_pages) { should == 7 } 112 | end 113 | 114 | context 'on query with order' do 115 | subject { User.page(5).all(:age.lte => 80, :order => [:age.asc]).per(13) } 116 | it('includes user with age 52') { should include(User.first(:age => 52)) } 117 | it('does not include user with age 51') { should_not include(User.first(:age => 51)) } 118 | it('includes user with age 52') { should include(User.first(:age => 64)) } 119 | it('does not include user with age 51') { should_not include(User.first(:age => 65)) } 120 | its(:current_page) { should == 5 } 121 | its('query.limit') { should == 13 } 122 | its('query.offset') { should == 52 } 123 | its(:total_count) { should == 81 } 124 | its(:total_pages) { should == 7 } 125 | end 126 | 127 | context 'on chained queries' do 128 | subject { User.all(:age.gte => 50).page(3).all(:age.lte => 80).per(13) } 129 | its(:current_page) { should == 3 } 130 | its('query.limit') { should == 13 } 131 | its('query.offset') { should == 26 } 132 | its(:total_count) { should == 31 } 133 | its(:total_pages) { should == 3 } 134 | end 135 | 136 | context 'on query on association' do 137 | subject { User[0].projects.page(3).all(:name.like => 'Project%').per(5) } 138 | its(:current_page) { should == 3 } 139 | its('query.limit') { should == 5 } 140 | its('query.offset') { should == 10 } 141 | its(:total_count) { should == 50 } 142 | its(:total_pages) { should == 10 } 143 | end 144 | 145 | context 'on query with association conditions' do 146 | subject { User.page(3).all(:projects => Project.all).per(5) } 147 | its(:current_page) { should == 3 } 148 | its('query.limit') { should == 5 } 149 | its('query.offset') { should == 10 } 150 | its(:total_count) { should == 50 } 151 | its(:total_pages) { should == 10 } 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/kaminari/helpers/paginator.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/inflector' 2 | require 'action_view' 3 | require 'action_view/log_subscriber' 4 | require 'action_view/context' 5 | require 'kaminari/helpers/tags' 6 | 7 | module Kaminari 8 | module Helpers 9 | # The main container tag 10 | class Paginator < Tag 11 | # so that this instance can actually "render" 12 | include ::ActionView::Context 13 | 14 | def initialize(template, options) #:nodoc: 15 | @window_options = {}.tap do |h| 16 | h[:window] = options.delete(:window) || options.delete(:inner_window) || Kaminari.config.window 17 | outer_window = options.delete(:outer_window) || Kaminari.config.outer_window 18 | h[:left] = options.delete(:left) || Kaminari.config.left 19 | h[:left] = outer_window if h[:left] == 0 20 | h[:right] = options.delete(:right) || Kaminari.config.right 21 | h[:right] = outer_window if h[:right] == 0 22 | end 23 | @template, @options = template, options 24 | @theme = @options[:theme] ? "#{@options[:theme]}/" : '' 25 | @options[:current_page] = PageProxy.new @window_options.merge(@options), @options[:current_page], nil 26 | #FIXME for compatibility. remove num_pages at some time in the future 27 | @options[:total_pages] ||= @options[:num_pages] 28 | @last = nil 29 | # initialize the output_buffer for Context 30 | @output_buffer = ActionView::OutputBuffer.new 31 | end 32 | 33 | # render given block as a view template 34 | def render(&block) 35 | instance_eval(&block) if @options[:total_pages] > 1 36 | @output_buffer 37 | end 38 | 39 | # enumerate each page providing PageProxy object as the block parameter 40 | # Because of performance reason, this doesn't actually enumerate all pages but pages that are seemingly relevant to the paginator. 41 | # "Relevant" pages are: 42 | # * pages inside the left outer window plus one for showing the gap tag 43 | # * pages inside the inner window plus one on the left plus one on the right for showing the gap tags 44 | # * pages inside the right outer window plus one for showing the gap tag 45 | def each_relevant_page 46 | return to_enum(:each_relevant_page) unless block_given? 47 | 48 | relevant_pages(@window_options.merge(@options)).each do |i| 49 | yield PageProxy.new(@window_options.merge(@options), i, @last) 50 | end 51 | end 52 | alias each_page each_relevant_page 53 | 54 | def relevant_pages(options) 55 | left_window_plus_one = 1.upto(options[:left] + 1).to_a 56 | right_window_plus_one = (options[:total_pages] - options[:right]).upto(options[:total_pages]).to_a 57 | inside_window_plus_each_sides = (options[:current_page] - options[:window] - 1).upto(options[:current_page] + options[:window] + 1).to_a 58 | 59 | (left_window_plus_one + inside_window_plus_each_sides + right_window_plus_one).uniq.sort.reject {|x| (x < 1) || (x > options[:total_pages])} 60 | end 61 | private :relevant_pages 62 | 63 | def page_tag(page) 64 | @last = Page.new @template, @options.merge(:page => page) 65 | end 66 | 67 | %w[first_page prev_page next_page last_page gap].each do |tag| 68 | eval <<-DEF 69 | def #{tag}_tag 70 | @last = #{tag.classify}.new @template, @options 71 | end 72 | DEF 73 | end 74 | 75 | def to_s #:nodoc: 76 | subscriber = ActionView::LogSubscriber.log_subscribers.detect {|ls| ls.is_a? ActionView::LogSubscriber} 77 | return super @window_options.merge(@options).merge :paginator => self unless subscriber 78 | 79 | # dirty hack to suppress logging render_partial 80 | class << subscriber 81 | alias_method :render_partial_with_logging, :render_partial 82 | # do nothing 83 | def render_partial(event); end 84 | end 85 | 86 | ret = super @window_options.merge(@options).merge :paginator => self 87 | 88 | class << subscriber 89 | alias_method :render_partial, :render_partial_with_logging 90 | undef :render_partial_with_logging 91 | end 92 | ret 93 | end 94 | 95 | # Wraps a "page number" and provides some utility methods 96 | class PageProxy 97 | include Comparable 98 | 99 | def initialize(options, page, last) #:nodoc: 100 | @options, @page, @last = options, page, last 101 | end 102 | 103 | # the page number 104 | def number 105 | @page 106 | end 107 | 108 | # current page or not 109 | def current? 110 | @page == @options[:current_page] 111 | end 112 | 113 | # the first page or not 114 | def first? 115 | @page == 1 116 | end 117 | 118 | # the last page or not 119 | def last? 120 | @page == @options[:total_pages] 121 | end 122 | 123 | # the previous page or not 124 | def prev? 125 | @page == @options[:current_page] - 1 126 | end 127 | 128 | # the next page or not 129 | def next? 130 | @page == @options[:current_page] + 1 131 | end 132 | 133 | # within the left outer window or not 134 | def left_outer? 135 | @page <= @options[:left] 136 | end 137 | 138 | # within the right outer window or not 139 | def right_outer? 140 | @options[:total_pages] - @page < @options[:right] 141 | end 142 | 143 | # inside the inner window or not 144 | def inside_window? 145 | (@options[:current_page] - @page).abs <= @options[:window] 146 | end 147 | 148 | # The last rendered tag was "truncated" or not 149 | def was_truncated? 150 | @last.is_a? Gap 151 | end 152 | 153 | def to_i 154 | number 155 | end 156 | 157 | def to_s 158 | number.to_s 159 | end 160 | 161 | def +(other) 162 | to_i + other.to_i 163 | end 164 | 165 | def -(other) 166 | to_i - other.to_i 167 | end 168 | 169 | def <=>(other) 170 | to_i <=> other.to_i 171 | end 172 | end 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/kaminari/helpers/action_view_extension.rb: -------------------------------------------------------------------------------- 1 | module Kaminari 2 | # = Helpers 3 | module ActionViewExtension 4 | # A helper that renders the pagination links. 5 | # 6 | # <%= paginate @articles %> 7 | # 8 | # ==== Options 9 | # * :window - The "inner window" size (4 by default). 10 | # * :outer_window - The "outer window" size (0 by default). 11 | # * :left - The "left outer window" size (0 by default). 12 | # * :right - The "right outer window" size (0 by default). 13 | # * :params - url_for parameters for the links (:controller, :action, etc.) 14 | # * :param_name - parameter name for page number in the links (:page by default) 15 | # * :remote - Ajax? (false by default) 16 | # * :ANY_OTHER_VALUES - Any other hash key & values would be directly passed into each tag as :locals value. 17 | def paginate(scope, options = {}, &block) 18 | paginator = Kaminari::Helpers::Paginator.new self, options.reverse_merge(:current_page => scope.current_page, :total_pages => scope.total_pages, :per_page => scope.limit_value, :param_name => Kaminari.config.param_name, :remote => false) 19 | paginator.to_s 20 | end 21 | 22 | # A simple "Twitter like" pagination link that creates a link to the previous page. 23 | # 24 | # ==== Examples 25 | # Basic usage: 26 | # 27 | # <%= link_to_previous_page @items, 'Previous Page' %> 28 | # 29 | # Ajax: 30 | # 31 | # <%= link_to_previous_page @items, 'Previous Page', :remote => true %> 32 | # 33 | # By default, it renders nothing if there are no more results on the previous page. 34 | # You can customize this output by passing a block. 35 | # 36 | # <%= link_to_previous_page @users, 'Previous Page' do %> 37 | # At the Beginning 38 | # <% end %> 39 | def link_to_previous_page(scope, name, options = {}, &block) 40 | params = options.delete(:params) || {} 41 | param_name = options.delete(:param_name) || Kaminari.config.param_name 42 | link_to_unless scope.first_page?, name, params.merge(param_name => (scope.current_page - 1)), options.reverse_merge(:rel => 'previous') do 43 | block.call if block 44 | end 45 | end 46 | 47 | # A simple "Twitter like" pagination link that creates a link to the next page. 48 | # 49 | # ==== Examples 50 | # Basic usage: 51 | # 52 | # <%= link_to_next_page @items, 'Next Page' %> 53 | # 54 | # Ajax: 55 | # 56 | # <%= link_to_next_page @items, 'Next Page', :remote => true %> 57 | # 58 | # By default, it renders nothing if there are no more results on the next page. 59 | # You can customize this output by passing a block. 60 | # 61 | # <%= link_to_next_page @users, 'Next Page' do %> 62 | # No More Pages 63 | # <% end %> 64 | def link_to_next_page(scope, name, options = {}, &block) 65 | params = options.delete(:params) || {} 66 | param_name = options.delete(:param_name) || Kaminari.config.param_name 67 | link_to_unless scope.last_page?, name, params.merge(param_name => (scope.current_page + 1)), options.reverse_merge(:rel => 'next') do 68 | block.call if block 69 | end 70 | end 71 | 72 | # Renders a helpful message with numbers of displayed vs. total entries. 73 | # Ported from mislav/will_paginate 74 | # 75 | # ==== Examples 76 | # Basic usage: 77 | # 78 | # <%= page_entries_info @posts %> 79 | # #-> Displaying posts 6 - 10 of 26 in total 80 | # 81 | # By default, the message will use the humanized class name of objects 82 | # in collection: for instance, "project types" for ProjectType models. 83 | # The namespace will be cutted out and only the last name will be used. 84 | # Override this with the :entry_name parameter: 85 | # 86 | # <%= page_entries_info @posts, :entry_name => 'item' %> 87 | # #-> Displaying items 6 - 10 of 26 in total 88 | def page_entries_info(collection, options = {}) 89 | entry_name = if options[:entry_name] 90 | options[:entry_name] 91 | elsif collection.empty? || collection.is_a?(PaginatableArray) 92 | 'entry' 93 | else 94 | if collection.respond_to? :model # DataMapper 95 | collection.model.model_name.human.downcase 96 | else # AR 97 | collection.model_name.human.downcase 98 | end 99 | end 100 | entry_name = entry_name.pluralize unless collection.total_count == 1 101 | 102 | if collection.total_pages < 2 103 | t('helpers.page_entries_info.one_page.display_entries', :entry_name => entry_name, :count => collection.total_count) 104 | else 105 | first = collection.offset_value + 1 106 | last = collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value 107 | t('helpers.page_entries_info.more_pages.display_entries', :entry_name => entry_name, :first => first, :last => last, :total => collection.total_count) 108 | end.html_safe 109 | end 110 | 111 | # Renders rel="next" and rel="prev" links to be used in the head. 112 | # 113 | # ==== Examples 114 | # Basic usage: 115 | # 116 | # In head: 117 | # 118 | # My Website 119 | # <%= yield :head %> 120 | # 121 | # 122 | # Somewhere in body: 123 | # <% content_for :head do %> 124 | # <%= rel_next_prev_link_tags @items %> 125 | # <% end %> 126 | # 127 | # #-> 128 | # 129 | def rel_next_prev_link_tags(scope, options = {}) 130 | params = options.delete(:params) || {} 131 | param_name = options.delete(:param_name) || Kaminari.config.param_name 132 | 133 | output = "" 134 | 135 | if !scope.first_page? && !scope.last_page? 136 | # If not first and not last, then output both links. 137 | output << '' 138 | output << '' 139 | elsif scope.first_page? 140 | # If first page, add next link unless last page. 141 | output << '' unless scope.last_page? 142 | else 143 | # If last page, add prev link unless first page. 144 | output << '' unless scope.first_page? 145 | end 146 | 147 | output.html_safe 148 | end 149 | 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/helpers/action_view_extension_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Kaminari::ActionViewExtension' do 4 | describe '#paginate' do 5 | before do 6 | 50.times {|i| User.create! :name => "user#{i}"} 7 | @users = User.page(1) 8 | end 9 | subject { helper.paginate @users, :params => {:controller => 'users', :action => 'index'} } 10 | it { should be_a String } 11 | 12 | context 'escaping the pagination for javascript' do 13 | it 'should escape for javascript' do 14 | lambda { helper.escape_javascript(helper.paginate @users, :params => {:controller => 'users', :action => 'index'}) }.should_not raise_error 15 | end 16 | end 17 | end 18 | 19 | describe '#link_to_previous_page' do 20 | before do 21 | 50.times {|i| User.create! :name => "user#{i}"} 22 | end 23 | context 'having previous pages' do 24 | before do 25 | @users = User.page(50) 26 | end 27 | context 'the default behaviour' do 28 | subject { helper.link_to_previous_page @users, 'Previous', :params => {:controller => 'users', :action => 'index'} } 29 | it { should be_a String } 30 | it { should match(/rel="previous"/) } 31 | end 32 | context 'overriding rel=' do 33 | subject { helper.link_to_previous_page @users, 'Previous', :rel => 'external', :params => {:controller => 'users', :action => 'index'} } 34 | it { should match(/rel="external"/) } 35 | end 36 | end 37 | context 'the first page' do 38 | before do 39 | @users = User.page(1) 40 | end 41 | subject { helper.link_to_previous_page @users, 'Previous', :params => {:controller => 'users', :action => 'index'} } 42 | it { should_not be } 43 | end 44 | end 45 | 46 | describe '#link_to_next_page' do 47 | before do 48 | 50.times {|i| User.create! :name => "user#{i}"} 49 | end 50 | context 'having more page' do 51 | before do 52 | @users = User.page(1) 53 | end 54 | context 'the default behaviour' do 55 | subject { helper.link_to_next_page @users, 'More', :params => {:controller => 'users', :action => 'index'} } 56 | it { should be_a String } 57 | it { should match(/rel="next"/) } 58 | end 59 | context 'overriding rel=' do 60 | subject { helper.link_to_next_page @users, 'More', :rel => 'external', :params => {:controller => 'users', :action => 'index'} } 61 | it { should match(/rel="external"/) } 62 | end 63 | end 64 | context 'the last page' do 65 | before do 66 | @users = User.page(2) 67 | end 68 | subject { helper.link_to_next_page @users, 'More', :params => {:controller => 'users', :action => 'index'} } 69 | it { should_not be } 70 | end 71 | end 72 | 73 | describe '#page_entries_info' do 74 | context 'on a model without namespace' do 75 | before do 76 | @users = User.page(1).per(25) 77 | end 78 | context 'having no entries' do 79 | subject { helper.page_entries_info @users, :params => {:controller => 'users', :action => 'index'} } 80 | it { should == 'No entries found' } 81 | 82 | context 'setting the entry name option to "member"' do 83 | subject { helper.page_entries_info @users, :entry_name => 'member', :params => {:controller => 'users', :action => 'index'} } 84 | it { should == 'No members found' } 85 | end 86 | end 87 | 88 | context 'having 1 entry' do 89 | before do 90 | User.create! :name => 'user1' 91 | @users = User.page(1).per(25) 92 | end 93 | subject { helper.page_entries_info @users, :params => {:controller => 'users', :action => 'index'} } 94 | it { should == 'Displaying 1 user' } 95 | 96 | context 'setting the entry name option to "member"' do 97 | subject { helper.page_entries_info @users, :entry_name => 'member', :params => {:controller => 'users', :action => 'index'} } 98 | it { should == 'Displaying 1 member' } 99 | end 100 | end 101 | 102 | context 'having more than 1 but less than a page of entries' do 103 | before do 104 | 10.times {|i| User.create! :name => "user#{i}"} 105 | @users = User.page(1).per(25) 106 | end 107 | subject { helper.page_entries_info @users, :params => {:controller => 'users', :action => 'index'} } 108 | it { should == 'Displaying all 10 users' } 109 | 110 | context 'setting the entry name option to "member"' do 111 | subject { helper.page_entries_info @users, :entry_name => 'member', :params => {:controller => 'users', :action => 'index'} } 112 | it { should == 'Displaying all 10 members' } 113 | end 114 | end 115 | 116 | context 'having more than one page of entries' do 117 | before do 118 | 50.times {|i| User.create! :name => "user#{i}"} 119 | end 120 | 121 | describe 'the first page' do 122 | before do 123 | @users = User.page(1).per(25) 124 | end 125 | subject { helper.page_entries_info @users, :params => {:controller => 'users', :action => 'index'} } 126 | it { should == 'Displaying users 1 - 25 of 50 in total' } 127 | 128 | context 'setting the entry name option to "member"' do 129 | subject { helper.page_entries_info @users, :entry_name => 'member', :params => {:controller => 'users', :action => 'index'} } 130 | it { should == 'Displaying members 1 - 25 of 50 in total' } 131 | end 132 | end 133 | 134 | describe 'the next page' do 135 | before do 136 | @users = User.page(2).per(25) 137 | end 138 | subject { helper.page_entries_info @users, :params => {:controller => 'users', :action => 'index'} } 139 | it { should == 'Displaying users 26 - 50 of 50 in total' } 140 | 141 | context 'setting the entry name option to "member"' do 142 | subject { helper.page_entries_info @users, :entry_name => 'member', :params => {:controller => 'users', :action => 'index'} } 143 | it { should == 'Displaying members 26 - 50 of 50 in total' } 144 | end 145 | end 146 | end 147 | end 148 | context 'on a model with namespace' do 149 | before do 150 | @addresses = User::Address.page(1).per(25) 151 | end 152 | context 'having no entries' do 153 | subject { helper.page_entries_info @addresses, :params => {:controller => 'addresses', :action => 'index'} } 154 | it { should == 'No entries found' } 155 | end 156 | 157 | context 'having 1 entry' do 158 | before do 159 | User::Address.create! 160 | @addresses = User::Address.page(1).per(25) 161 | end 162 | subject { helper.page_entries_info @addresses, :params => {:controller => 'addresses', :action => 'index'} } 163 | it { should == 'Displaying 1 address' } 164 | 165 | context 'setting the entry name option to "place"' do 166 | subject { helper.page_entries_info @addresses, :entry_name => 'place', :params => {:controller => 'addresses', :action => 'index'} } 167 | it { should == 'Displaying 1 place' } 168 | end 169 | end 170 | 171 | context 'having more than 1 but less than a page of entries' do 172 | before do 173 | 10.times {|i| User::Address.create!} 174 | @addresses = User::Address.page(1).per(25) 175 | end 176 | subject { helper.page_entries_info @addresses, :params => {:controller => 'addresses', :action => 'index'} } 177 | it { should == 'Displaying all 10 addresses' } 178 | 179 | context 'setting the entry name option to "place"' do 180 | subject { helper.page_entries_info @addresses, :entry_name => 'place', :params => {:controller => 'addresses', :action => 'index'} } 181 | it { should == 'Displaying all 10 places' } 182 | end 183 | end 184 | 185 | context 'having more than one page of entries' do 186 | before do 187 | 50.times {|i| User::Address.create!} 188 | end 189 | 190 | describe 'the first page' do 191 | before do 192 | @addresses = User::Address.page(1).per(25) 193 | end 194 | subject { helper.page_entries_info @addresses, :params => {:controller => 'addresses', :action => 'index'} } 195 | it { should == 'Displaying addresses 1 - 25 of 50 in total' } 196 | 197 | context 'setting the entry name option to "place"' do 198 | subject { helper.page_entries_info @addresses, :entry_name => 'place', :params => {:controller => 'addresses', :action => 'index'} } 199 | it { should == 'Displaying places 1 - 25 of 50 in total' } 200 | end 201 | end 202 | 203 | describe 'the next page' do 204 | before do 205 | @addresses = User::Address.page(2).per(25) 206 | end 207 | subject { helper.page_entries_info @addresses, :params => {:controller => 'addresses', :action => 'index'} } 208 | it { should == 'Displaying addresses 26 - 50 of 50 in total' } 209 | 210 | context 'setting the entry name option to "place"' do 211 | subject { helper.page_entries_info @addresses, :entry_name => 'place', :params => {:controller => 'addresses', :action => 'index'} } 212 | it { should == 'Displaying places 26 - 50 of 50 in total' } 213 | end 214 | end 215 | end 216 | end 217 | 218 | context 'on a PaginatableArray' do 219 | before do 220 | @numbers = Kaminari.paginate_array(%w{one two three}).page(1) 221 | end 222 | subject { helper.page_entries_info @numbers } 223 | it { should == 'Displaying all 3 entries' } 224 | end 225 | end 226 | 227 | describe '#rel_next_prev_link_tags' do 228 | before do 229 | 75.times {|i| User.create! :name => "user#{i}"} 230 | end 231 | context 'the first page' do 232 | before do 233 | @users = User.page(1).per(25) 234 | end 235 | 236 | subject { helper.rel_next_prev_link_tags @users, :params => {:controller => 'users', :action => 'index'} } 237 | it { should be_a String } 238 | it { should match(/rel="next"/) } 239 | it { should_not match(/rel="prev"/) } 240 | end 241 | context 'the middle page' do 242 | before do 243 | @users = User.page(2).per(25) 244 | end 245 | 246 | subject { helper.rel_next_prev_link_tags @users, :params => {:controller => 'users', :action => 'index'} } 247 | it { should be_a String } 248 | it { should match(/rel="next"/) } 249 | it { should match(/rel="prev"/) } 250 | end 251 | context 'the last page' do 252 | before do 253 | @users = User.page(3).per(25) 254 | end 255 | 256 | subject { helper.rel_next_prev_link_tags @users, :params => {:controller => 'users', :action => 'index'} } 257 | it { should be_a String } 258 | it { should_not match(/rel="next"/) } 259 | it { should match(/rel="prev"/) } 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | == 0.14.1 2 | 3 | * Changed the default "truncation" String from "..." to … #264 [pjaspers] 4 | 5 | * The theme generator is Github API v3 compatible now! #279 [eitoball] 6 | 7 | * Made Kaminari.config.default_per_page changeable again #280 [yuki24] 8 | 9 | == 0.14.0 10 | 11 | * Grape framework support! #218 [mrplum] 12 | 13 | * Mongoid 3 ready! #238 [shingara] 14 | 15 | * Added link_to_previous_page helper #191 [timgremore] 16 | 17 | * Added helper to generate rel="next" and rel="prev" link tags for SEO #200 18 | [joe1chen] 19 | 20 | * Added `max_per_page` configuration option #274 [keiko0713] 21 | This would be useful for the case when you are using user input `per_page` 22 | value but want to impose the upper bound. 23 | 24 | * Added I18n to page_entries_info #207 [plribeiro3000] 25 | 26 | * Changed method name "num_pages" to "total_pages" 27 | num_pages" is still available as an alias of "total_pages", but will be 28 | deprecated or removed in some future version. 29 | 30 | * Changed the way page_entries_info behave so it can show appropriate names 31 | for models with namespace #207 [plribeiro3000] 32 | 33 | * Added html_safe to page_entries_info helper #190 [lucapette] 34 | 35 | * Fixed displayed number of items on each page w/ Mongoid 2.4.x and 36 | MongoMapper #194 [dblock] 37 | 38 | * Removed a unused local variable from templates from default tamplate #245 [juno] 39 | 40 | * Fixed page_entry_info to use the value of `entry_name` option when given 41 | collection is empty or a PaginatableArray #265, #277 [eitoball] 42 | 43 | * Added require 'dm-aggregates' in DataMapper hook #259 [shingara] 44 | 45 | == 0.13.0 46 | 47 | * Rails 3.2 ready! #180 [slbug] 48 | 49 | * DataMapper support! #149 [NoICE, Ragmaanir] 50 | 51 | * Sinatra & Padrino support! #179 [udzura, mlightner, aereal] 52 | 53 | * Added mongoid embedded documents support! #155 [yuki24] 54 | 55 | * Added `each_relevant_page` that only visits pages in the inner or outer 56 | windows #154 [cbeer] 57 | Performance improved, particularly with very large number of pages. 58 | 59 | * Memoize count for AR when calling `total_count` #138 [sarmiena] 60 | Increases performance for large datasets. 61 | 62 | * Added `page_entries_info` view helper #140 [jeffreyiacono] 63 | Example: 64 | <%= page_entries_info @posts %> 65 | #=> Displaying posts 6 - 10 of 26 in total 66 | 67 | * Added `link_to_next_page` helper method that simply links to the next page 68 | Example: 69 | <%= link_to_next_page @posts, 'More' %> 70 | #=> 71 | 72 | * Let one override the `rel` attribute for 'link_to_next_page` helper #177 73 | [webmat] 74 | 75 | * Added `total_count` param for PaginatableArray. Useful for when working with 76 | RSolr #141 [samdalton] 77 | 78 | * Changed `Kaminari.paginate_array` API to take a Hash `options` 79 | And specifying :limit & :offset immediately builds a pagination ready object 80 | Example: 81 | # the following two are equivalent. Use whichever you like 82 | Kaminari.paginate_array((1..100).to_a, limit: 10, offset: 10) 83 | Kaminari.paginate_array((1..100).to_a).page(2).per(10) 84 | 85 | * Added `padding` method to skip an arbitrary record count #60 [aaronjensen] 86 | Example: 87 | User.page(2).per(10).padding(3) # this will return users 14..23 88 | 89 | * Made the pagination method name (defaulted to `page`) configurable #57, #162 90 | Example: 91 | # you can use the config file and its generator for this 92 | Kaminari.config.page_method_name = :paging 93 | Article.paging(3).per(30) 94 | 95 | * Only add extensions to direct descendents of ActiveRecord::Base #108 96 | [seejohnrun] 97 | 98 | * AR models that were subclassed before Kaminari::ActiveRecordExtension is 99 | included pick up the extensions #119 [pivotal-casebook] 100 | 101 | * Avoid overwriting AR::Base inherited method #165 [briandmcnabb] 102 | 103 | * Stopped depending on Rails gem #159 [alsemyonov] 104 | 105 | * introduced Travis CI #181 [hsbt] 106 | 107 | == 0.12.4 108 | 109 | * Support for config.param_name as lambda #102 [ajrkerr] 110 | 111 | * Stop duplicating order_values #65 [zettabyte] 112 | 113 | * Preserve select value (e.g. "distinct") when counting #77, #104 [tbeauvais, 114 | beatlevic] 115 | 116 | == 0.12.3 117 | 118 | * Haml 3.1 Support #96 [FlyboyArt, sonic921] 119 | 120 | == 0.12.2 121 | 122 | * Added MongoMapper Support #101 [hamin] 123 | 124 | * Add first_page? and last_page? to page_scope_methods #51 [holinnn] 125 | 126 | * Make sure that the paginate helper always returns a String #99 [Draiken] 127 | 128 | * Don't remove includes scopes from count if they are needed #100 [flop] 129 | 130 | == 0.12.1 131 | 132 | * Slim template support #93 [detrain] 133 | 134 | * Use Kaminari.config to specify default value for param_name #94 [avsej] 135 | 136 | * Fixed "super called outside of method" error happened in particular versions 137 | of Ruby 1.8.7 #91 [Skulli] 138 | 139 | * _paginate.html.erb isn't rendered with custom theme #97 [danlunde] 140 | 141 | == 0.12.0 142 | 143 | * General configuration options #41 #62 [javierv, iain] 144 | You can now globally override some default values such as default_per_page, 145 | window, etc. via configuration file. 146 | Also, here comes a generator command that generates the default 147 | configuration file into your app's config/initilizers directory. 148 | 149 | * Generic pagination support for Array object #47 #68 #74 [lda, ened, jianlin] 150 | You can now paginate through any kind of Arrayish object in this way: 151 | Kaminari.paginate_array(my_array_object).page(params[:page]).per(10) 152 | 153 | * Fixed a serious performance regression in 0.11.0 [ankane] 154 | There was a critical performance issue on #count method in 0.11.0 gem. 155 | 156 | * Bugfix: Pass the real @params to url_for #90 [utkarshkukreti] 157 | 158 | * Fixed a gem packaging problem (circular dependency) 159 | There was a packaging problem with Kaminari 0.11.0 that the gem depends on 160 | Kaminari gem. Maybe Jeweler + "gemspec" method didn't work well... 161 | 162 | == 0.11.0 163 | 164 | This release contains several backward incompatibilities on template API. 165 | You probably need to update your existing templates if you're already using 166 | your own custom theme. 167 | 168 | * Merge _current_page, _first_page_link, _last_page_link and _page_link into 169 | one _page partial #28 [GarthSnyder] 170 | 171 | * Add real first/last page links, and use them by default instead of outer 172 | window #30 [GarthSnyder] 173 | 174 | * The disabled items should simply not be emitted, even as an empty span #30 175 | [GarthSnyder] 176 | 177 | * Skip :order in #count_all so complex groups with generated columns don't 178 | blow up in SQL-land #61 [keeran, Empact] 179 | 180 | * Ignore :include in #count_all to make it work better with polymorphic eager 181 | loading #80 [njakobsen] 182 | 183 | * Quick fix on #count to return the actual number of records on AR 3.0 #45 #50 184 | 185 | * Removed "TERRIBLE HORRIBLE NO GOOD VERY BAD HACK" #82 [janx, flop, pda] 186 | 187 | * Allow for Multiple Themes #64 [tmilewski] 188 | 189 | * Themes can contain the whole application directory structure now 190 | 191 | * Use gemspec method in Gemfile [p_elliott] 192 | 193 | == 0.10.4 194 | 195 | * Do not break ActiveRecord::Base.descendants, by making sure to call super 196 | from ActiveRecord::Base.inherited #34 [rolftimmermans] 197 | 198 | * Fixed vanishing mongoid criteria after calling page() #26 [tyok] 199 | 200 | == 0.10.3 201 | 202 | * Fixed a bug that total_count() didn't work when chained with group() scope 203 | #21 [jgeiger] 204 | 205 | * Fixed a bug that the paginate helper didn't work properly with an Ajax call 206 | #23 [hjuskewycz] 207 | 208 | == 0.10.2 209 | 210 | * Added :param_name option to the pagination helper #10 [ivanvr] 211 | Example: 212 | = paginate @users, :param_name => :pagina 213 | 214 | == 0.10.1 215 | 216 | * Fixed a bug that the whole