kaminari-rails4 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gemtest +0 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/CHANGELOG +351 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +300 -0
- data/Rakefile +42 -0
- data/app/helpers/kaminari_helper.rb +5 -0
- data/app/views/kaminari/_first_page.html.erb +11 -0
- data/app/views/kaminari/_first_page.html.haml +9 -0
- data/app/views/kaminari/_first_page.html.slim +10 -0
- data/app/views/kaminari/_gap.html.erb +8 -0
- data/app/views/kaminari/_gap.html.haml +8 -0
- data/app/views/kaminari/_gap.html.slim +9 -0
- data/app/views/kaminari/_last_page.html.erb +11 -0
- data/app/views/kaminari/_last_page.html.haml +9 -0
- data/app/views/kaminari/_last_page.html.slim +10 -0
- data/app/views/kaminari/_next_page.html.erb +11 -0
- data/app/views/kaminari/_next_page.html.haml +9 -0
- data/app/views/kaminari/_next_page.html.slim +10 -0
- data/app/views/kaminari/_page.html.erb +12 -0
- data/app/views/kaminari/_page.html.haml +10 -0
- data/app/views/kaminari/_page.html.slim +11 -0
- data/app/views/kaminari/_paginator.html.erb +23 -0
- data/app/views/kaminari/_paginator.html.haml +18 -0
- data/app/views/kaminari/_paginator.html.slim +19 -0
- data/app/views/kaminari/_prev_page.html.erb +11 -0
- data/app/views/kaminari/_prev_page.html.haml +9 -0
- data/app/views/kaminari/_prev_page.html.slim +10 -0
- data/config/locales/kaminari.yml +19 -0
- data/gemfiles/active_record_30.gemfile +7 -0
- data/gemfiles/active_record_31.gemfile +7 -0
- data/gemfiles/active_record_32.gemfile +10 -0
- data/gemfiles/active_record_40.gemfile +8 -0
- data/gemfiles/data_mapper_12.gemfile +15 -0
- data/gemfiles/mongo_mapper.gemfile +10 -0
- data/gemfiles/mongoid_24.gemfile +7 -0
- data/gemfiles/mongoid_30.gemfile +12 -0
- data/gemfiles/sinatra.gemfile +13 -0
- data/kaminari.gemspec +36 -0
- data/lib/generators/kaminari/config_generator.rb +16 -0
- data/lib/generators/kaminari/templates/kaminari_config.rb +10 -0
- data/lib/generators/kaminari/views_generator.rb +118 -0
- data/lib/kaminari.rb +38 -0
- data/lib/kaminari/config.rb +51 -0
- data/lib/kaminari/engine.rb +4 -0
- data/lib/kaminari/grape.rb +4 -0
- data/lib/kaminari/helpers/action_view_extension.rb +151 -0
- data/lib/kaminari/helpers/paginator.rb +186 -0
- data/lib/kaminari/helpers/sinatra_helpers.rb +144 -0
- data/lib/kaminari/helpers/tags.rb +95 -0
- data/lib/kaminari/hooks.rb +33 -0
- data/lib/kaminari/models/active_record_extension.rb +22 -0
- data/lib/kaminari/models/active_record_model_extension.rb +20 -0
- data/lib/kaminari/models/active_record_relation_methods.rb +29 -0
- data/lib/kaminari/models/array_extension.rb +58 -0
- data/lib/kaminari/models/configuration_methods.rb +48 -0
- data/lib/kaminari/models/data_mapper_collection_methods.rb +15 -0
- data/lib/kaminari/models/data_mapper_extension.rb +48 -0
- data/lib/kaminari/models/mongo_mapper_extension.rb +18 -0
- data/lib/kaminari/models/mongoid_criteria_methods.rb +23 -0
- data/lib/kaminari/models/mongoid_extension.rb +33 -0
- data/lib/kaminari/models/page_scope_methods.rb +70 -0
- data/lib/kaminari/models/plucky_criteria_methods.rb +18 -0
- data/lib/kaminari/railtie.rb +7 -0
- data/lib/kaminari/sinatra.rb +5 -0
- data/lib/kaminari/version.rb +3 -0
- data/spec/config/config_spec.rb +91 -0
- data/spec/fake_app/active_record/config.rb +3 -0
- data/spec/fake_app/active_record/models.rb +57 -0
- data/spec/fake_app/data_mapper/config.rb +1 -0
- data/spec/fake_app/data_mapper/models.rb +27 -0
- data/spec/fake_app/mongo_mapper/config.rb +2 -0
- data/spec/fake_app/mongo_mapper/models.rb +9 -0
- data/spec/fake_app/mongoid/config.rb +16 -0
- data/spec/fake_app/mongoid/models.rb +22 -0
- data/spec/fake_app/rails_app.rb +56 -0
- data/spec/fake_app/sinatra_app.rb +22 -0
- data/spec/fake_gem.rb +4 -0
- data/spec/helpers/action_view_extension_spec.rb +262 -0
- data/spec/helpers/helpers_spec.rb +135 -0
- data/spec/helpers/sinatra_helpers_spec.rb +170 -0
- data/spec/helpers/tags_spec.rb +140 -0
- data/spec/models/active_record/active_record_relation_methods_spec.rb +47 -0
- data/spec/models/active_record/default_per_page_spec.rb +32 -0
- data/spec/models/active_record/max_pages_spec.rb +23 -0
- data/spec/models/active_record/max_per_page_spec.rb +32 -0
- data/spec/models/active_record/scopes_spec.rb +242 -0
- data/spec/models/array_spec.rb +150 -0
- data/spec/models/data_mapper/data_mapper_spec.rb +179 -0
- data/spec/models/mongo_mapper/mongo_mapper_spec.rb +84 -0
- data/spec/models/mongoid/mongoid_spec.rb +126 -0
- data/spec/requests/users_spec.rb +53 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/spec_helper_for_sinatra.rb +33 -0
- data/spec/support/database_cleaner.rb +16 -0
- data/spec/support/matchers.rb +52 -0
- metadata +300 -0
data/kaminari.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
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-rails4'
|
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-rails4'
|
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
|
@@ -0,0 +1,16 @@
|
|
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 <<DESC
|
7
|
+
Description:
|
8
|
+
Copies Kaminari configuration file to your application's initializer directory.
|
9
|
+
DESC
|
10
|
+
|
11
|
+
def copy_config_file
|
12
|
+
template 'kaminari_config.rb', 'config/initializers/kaminari_config.rb'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,118 @@
|
|
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
|
data/lib/kaminari.rb
ADDED
@@ -0,0 +1,38 @@
|
|
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/paginator'
|
30
|
+
require 'kaminari/models/page_scope_methods'
|
31
|
+
require 'kaminari/models/configuration_methods'
|
32
|
+
require 'kaminari/hooks'
|
33
|
+
|
34
|
+
# if not using Railtie, call `Kaminari::Hooks.init` directly
|
35
|
+
if defined? Rails
|
36
|
+
require 'kaminari/railtie'
|
37
|
+
require 'kaminari/engine'
|
38
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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
|
+
config_accessor :max_pages
|
28
|
+
|
29
|
+
def param_name
|
30
|
+
config.param_name.respond_to?(:call) ? config.param_name.call : config.param_name
|
31
|
+
end
|
32
|
+
|
33
|
+
# define param_name writer (copied from AS::Configurable)
|
34
|
+
writer, line = 'def param_name=(value); config.param_name = value; end', __LINE__
|
35
|
+
singleton_class.class_eval writer, __FILE__, line
|
36
|
+
class_eval writer, __FILE__, line
|
37
|
+
end
|
38
|
+
|
39
|
+
# this is ugly. why can't we pass the default value to config_accessor...?
|
40
|
+
configure do |config|
|
41
|
+
config.default_per_page = 25
|
42
|
+
config.max_per_page = nil
|
43
|
+
config.window = 4
|
44
|
+
config.outer_window = 0
|
45
|
+
config.left = 0
|
46
|
+
config.right = 0
|
47
|
+
config.page_method_name = :page
|
48
|
+
config.param_name = :page
|
49
|
+
config.max_pages = nil
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Kaminari
|
2
|
+
# = Helpers
|
3
|
+
module ActionViewExtension
|
4
|
+
# A helper that renders the pagination links.
|
5
|
+
#
|
6
|
+
# <%= paginate @articles %>
|
7
|
+
#
|
8
|
+
# ==== Options
|
9
|
+
# * <tt>:window</tt> - The "inner window" size (4 by default).
|
10
|
+
# * <tt>:outer_window</tt> - The "outer window" size (0 by default).
|
11
|
+
# * <tt>:left</tt> - The "left outer window" size (0 by default).
|
12
|
+
# * <tt>:right</tt> - The "right outer window" size (0 by default).
|
13
|
+
# * <tt>:params</tt> - url_for parameters for the links (:controller, :action, etc.)
|
14
|
+
# * <tt>:param_name</tt> - parameter name for page number in the links (:page by default)
|
15
|
+
# * <tt>:remote</tt> - Ajax? (false by default)
|
16
|
+
# * <tt>:ANY_OTHER_VALUES</tt> - 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
|
+
# <span>At the Beginning</span>
|
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
|
+
# <span>No More Pages</span>
|
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 <tt>:entry_name</tt> 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?(::Kaminari::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
|
+
# <head>
|
118
|
+
# <title>My Website</title>
|
119
|
+
# <%= yield :head %>
|
120
|
+
# </head>
|
121
|
+
#
|
122
|
+
# Somewhere in body:
|
123
|
+
# <% content_for :head do %>
|
124
|
+
# <%= rel_next_prev_link_tags @items %>
|
125
|
+
# <% end %>
|
126
|
+
#
|
127
|
+
# #-> <link rel="next" href="/items/page/3" /><link rel="prev" href="/items/page/1" />
|
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 << '<link rel="next" href="' + url_for(params.merge(param_name => (scope.current_page + 1))) + '"/>'
|
138
|
+
output << '<link rel="prev" href="' + url_for(params.merge(param_name => (scope.current_page - 1))) + '"/>'
|
139
|
+
elsif scope.first_page?
|
140
|
+
# If first page, add next link unless last page.
|
141
|
+
output << '<link rel="next" href="' + url_for(params.merge(param_name => (scope.current_page + 1))) + '"/>' unless scope.last_page?
|
142
|
+
else
|
143
|
+
# If last page, add prev link unless first page.
|
144
|
+
output << '<link rel="prev" href="' + url_for(params.merge(param_name => (scope.current_page - 1))) + '"/>' unless scope.first_page?
|
145
|
+
end
|
146
|
+
|
147
|
+
output.html_safe
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,186 @@
|
|
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
|
+
@options[:num_pages] ||= @options[:total_pages]
|
29
|
+
@last = nil
|
30
|
+
# initialize the output_buffer for Context
|
31
|
+
@output_buffer = ActionView::OutputBuffer.new
|
32
|
+
end
|
33
|
+
|
34
|
+
# render given block as a view template
|
35
|
+
def render(&block)
|
36
|
+
instance_eval(&block) if @options[:total_pages] > 1
|
37
|
+
@output_buffer
|
38
|
+
end
|
39
|
+
|
40
|
+
# enumerate each page providing PageProxy object as the block parameter
|
41
|
+
# Because of performance reason, this doesn't actually enumerate all pages but pages that are seemingly relevant to the paginator.
|
42
|
+
# "Relevant" pages are:
|
43
|
+
# * pages inside the left outer window plus one for showing the gap tag
|
44
|
+
# * pages inside the inner window plus one on the left plus one on the right for showing the gap tags
|
45
|
+
# * pages inside the right outer window plus one for showing the gap tag
|
46
|
+
def each_relevant_page
|
47
|
+
return to_enum(:each_relevant_page) unless block_given?
|
48
|
+
|
49
|
+
relevant_pages(@window_options.merge(@options)).each do |i|
|
50
|
+
yield PageProxy.new(@window_options.merge(@options), i, @last)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
alias each_page each_relevant_page
|
54
|
+
|
55
|
+
def relevant_pages(options)
|
56
|
+
left_window_plus_one = 1.upto(options[:left] + 1).to_a
|
57
|
+
right_window_plus_one = (options[:total_pages] - options[:right]).upto(options[:total_pages]).to_a
|
58
|
+
inside_window_plus_each_sides = (options[:current_page] - options[:window] - 1).upto(options[:current_page] + options[:window] + 1).to_a
|
59
|
+
|
60
|
+
(left_window_plus_one + inside_window_plus_each_sides + right_window_plus_one).uniq.sort.reject {|x| (x < 1) || (x > options[:total_pages])}
|
61
|
+
end
|
62
|
+
private :relevant_pages
|
63
|
+
|
64
|
+
def page_tag(page)
|
65
|
+
@last = Page.new @template, @options.merge(:page => page)
|
66
|
+
end
|
67
|
+
|
68
|
+
%w[first_page prev_page next_page last_page gap].each do |tag|
|
69
|
+
eval <<-DEF
|
70
|
+
def #{tag}_tag
|
71
|
+
@last = #{tag.classify}.new @template, @options
|
72
|
+
end
|
73
|
+
DEF
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s #:nodoc:
|
77
|
+
subscriber = ActionView::LogSubscriber.log_subscribers.detect {|ls| ls.is_a? ActionView::LogSubscriber}
|
78
|
+
|
79
|
+
# There is a logging subscriber
|
80
|
+
# and we don't want it to log render_partial
|
81
|
+
# It is threadsafe, but might not repress logging
|
82
|
+
# consistently in a high-load environment
|
83
|
+
if subscriber
|
84
|
+
unless defined? subscriber.render_partial_with_logging
|
85
|
+
class << subscriber
|
86
|
+
alias_method :render_partial_with_logging, :render_partial
|
87
|
+
attr_accessor :render_without_logging
|
88
|
+
# ugly hack to make a renderer where
|
89
|
+
# we can turn logging on or off
|
90
|
+
def render_partial(event)
|
91
|
+
render_partial_with_logging(event) unless render_without_logging
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
subscriber.render_without_logging = true
|
97
|
+
ret = super @window_options.merge(@options).merge :paginator => self
|
98
|
+
subscriber.render_without_logging = false
|
99
|
+
|
100
|
+
ret
|
101
|
+
else
|
102
|
+
super @window_options.merge(@options).merge :paginator => self
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Wraps a "page number" and provides some utility methods
|
107
|
+
class PageProxy
|
108
|
+
include Comparable
|
109
|
+
|
110
|
+
def initialize(options, page, last) #:nodoc:
|
111
|
+
@options, @page, @last = options, page, last
|
112
|
+
end
|
113
|
+
|
114
|
+
# the page number
|
115
|
+
def number
|
116
|
+
@page
|
117
|
+
end
|
118
|
+
|
119
|
+
# current page or not
|
120
|
+
def current?
|
121
|
+
@page == @options[:current_page]
|
122
|
+
end
|
123
|
+
|
124
|
+
# the first page or not
|
125
|
+
def first?
|
126
|
+
@page == 1
|
127
|
+
end
|
128
|
+
|
129
|
+
# the last page or not
|
130
|
+
def last?
|
131
|
+
@page == @options[:total_pages]
|
132
|
+
end
|
133
|
+
|
134
|
+
# the previous page or not
|
135
|
+
def prev?
|
136
|
+
@page == @options[:current_page] - 1
|
137
|
+
end
|
138
|
+
|
139
|
+
# the next page or not
|
140
|
+
def next?
|
141
|
+
@page == @options[:current_page] + 1
|
142
|
+
end
|
143
|
+
|
144
|
+
# within the left outer window or not
|
145
|
+
def left_outer?
|
146
|
+
@page <= @options[:left]
|
147
|
+
end
|
148
|
+
|
149
|
+
# within the right outer window or not
|
150
|
+
def right_outer?
|
151
|
+
@options[:total_pages] - @page < @options[:right]
|
152
|
+
end
|
153
|
+
|
154
|
+
# inside the inner window or not
|
155
|
+
def inside_window?
|
156
|
+
(@options[:current_page] - @page).abs <= @options[:window]
|
157
|
+
end
|
158
|
+
|
159
|
+
# The last rendered tag was "truncated" or not
|
160
|
+
def was_truncated?
|
161
|
+
@last.is_a? Gap
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_i
|
165
|
+
number
|
166
|
+
end
|
167
|
+
|
168
|
+
def to_s
|
169
|
+
number.to_s
|
170
|
+
end
|
171
|
+
|
172
|
+
def +(other)
|
173
|
+
to_i + other.to_i
|
174
|
+
end
|
175
|
+
|
176
|
+
def -(other)
|
177
|
+
to_i - other.to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
def <=>(other)
|
181
|
+
to_i <=> other.to_i
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|