cursor_pagination 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +105 -0
- data/Rakefile +1 -0
- data/app/helpers/cursor_pagination_helper.rb +3 -0
- data/cursor_pagination.gemspec +28 -0
- data/lib/cursor_pagination.rb +7 -0
- data/lib/cursor_pagination/action_view_helper.rb +25 -0
- data/lib/cursor_pagination/active_record_extension.rb +24 -0
- data/lib/cursor_pagination/active_record_model_extension.rb +34 -0
- data/lib/cursor_pagination/page_scope_methods.rb +36 -0
- data/lib/cursor_pagination/version.rb +3 -0
- data/spec/fake_app/active_record/config.rb +2 -0
- data/spec/fake_app/active_record/models.rb +17 -0
- data/spec/fake_app/rails.rb +30 -0
- data/spec/helpers/cursor_pagination_helper_spec.rb +51 -0
- data/spec/models/entity_spec.rb +93 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/spec_shared.rb +16 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0975daf496d378c472103614276ccbcb01b30c9c
|
4
|
+
data.tar.gz: 054a13ee7d3149d8b0174b1ff776219c1be0443c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ce77e48e37546b7c6192ab9f5e5a4b25f10b4dbfe1f78d87d9e24a9b9608cc96d02475b4f3adc1924c607197d1c443c88e169bf5da12ac0ce0d14d17b6d3cfee
|
7
|
+
data.tar.gz: 9e6024bf149e93ef1762e56964145172a892cb4ece9ea8c349bd3cead53330be9b3cc30dcf98927209c883c5159aab4a4f468496072c451d5f8ba5fbba084af0
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sergey Kukunin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# CursorPagination
|
2
|
+
|
3
|
+
ActiveRecord plugin for cursor based pagination. It uses specific model's column and rpp (results per page) to paginate your content.
|
4
|
+
|
5
|
+
The main advantage against traditional pagination (limmit and offset), that the one URL on specific page will contain the data set, despite the newly added entities. It may be useful on the projects, where new entities are added often, and specific page now isn't specific page tomorrow.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'cursor_pagination'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install cursor_pagination
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Just use `cursor` scope on your model chain:
|
24
|
+
|
25
|
+
User.where(active: true).cursor(params[:cursor]).per(20)
|
26
|
+
|
27
|
+
It will get 20 users by passed cursor. You can omit `per` scope:
|
28
|
+
|
29
|
+
User.where(active: true).cursor(params[:cursor])
|
30
|
+
|
31
|
+
In this case, the limit of entities per page will be 25 by default.
|
32
|
+
|
33
|
+
You can pass options as second argument for `cursor` scope
|
34
|
+
|
35
|
+
User.order('id DESC').cursor(params[:cursor], reverse: true).per(20)
|
36
|
+
|
37
|
+
## How it works?
|
38
|
+
|
39
|
+
Actually, cursor is column value of specific entity, it uses to get all entities, later than specific. For example, if you have the Users set with IDs from 1 to 5,
|
40
|
+
|
41
|
+
User.cursor(2).per(20)
|
42
|
+
|
43
|
+
will return maximum 20 users after ID 2 (in this case, users with IDs 3, 4 and 5).
|
44
|
+
|
45
|
+
**Make sure that your objects are ordered by cursored column. If you use DESC order, use `reverse: true` option in `cursor` scope.**
|
46
|
+
|
47
|
+
## Options
|
48
|
+
|
49
|
+
At this point, `cursor` scope accepts these options:
|
50
|
+
|
51
|
+
* `reverse`: Set it to true, if your set are ordered descendingly (_DESC_). Default: _false_
|
52
|
+
* `column`: column value of cursor. For example, if you order your data set by *updated_at*, set *updated_at* column for cursor. Default: _id_
|
53
|
+
|
54
|
+
## Scope methods
|
55
|
+
|
56
|
+
* `first_page?/last_page?` - **true/false**
|
57
|
+
|
58
|
+
```
|
59
|
+
@users = User.cursor(params[:cursor]).per(20)
|
60
|
+
@users.first_page?
|
61
|
+
```
|
62
|
+
|
63
|
+
* `next_cursor/previous_cursor` - **cursor, nil or -1**. Returns the column value for cursor of next/previous page.
|
64
|
+
_nil_ is valid cursor too. It means first page. If cursor is unavailable (there isn't pages anymore), returns _-1_.
|
65
|
+
|
66
|
+
## Helpers
|
67
|
+
|
68
|
+
* `next_cursor_url/previous_cursor_url` - **string**
|
69
|
+
|
70
|
+
```
|
71
|
+
next_cursor_url(scope, url_options = {})
|
72
|
+
previous_cursor_url(scope, url_options = {})
|
73
|
+
```
|
74
|
+
|
75
|
+
Returns the URL for next/previous cursor page or nil, if there isn't next/previous cursor available.
|
76
|
+
|
77
|
+
```
|
78
|
+
<%= next_cursor_url(@users) %> # users/?cursor=3
|
79
|
+
<%= previous_cursor_url(@users) %> # users/?cursor=1
|
80
|
+
```
|
81
|
+
|
82
|
+
* `next_cursor_link/previous_cursor_link` - **string**
|
83
|
+
|
84
|
+
```
|
85
|
+
next_cursor_link(scope, name, url_options = {}, html_options = {})
|
86
|
+
previous_cursor_link(scope, name, url_options = {}, html_options = {})
|
87
|
+
```
|
88
|
+
|
89
|
+
Returns the A html element with URL on next/previous cursor or nil, if there isn't next/previous cursor available. Accepts the same arguments, as `link_to` helper method, but scope object as first argument.
|
90
|
+
|
91
|
+
```
|
92
|
+
# <a href="users/?cursor=3" rel="next">Next Page</a>
|
93
|
+
<%= next_cursor_link(scope, 'Next Page') %>
|
94
|
+
# <a href="users/?cursor=1" rel="previous">Previous Page</a>
|
95
|
+
<%= previous_cursor_link(scope, 'Previous Page') %>
|
96
|
+
```
|
97
|
+
|
98
|
+
|
99
|
+
## Contributing
|
100
|
+
|
101
|
+
1. Fork it
|
102
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
103
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
104
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
105
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cursor_pagination/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "cursor_pagination"
|
8
|
+
spec.version = CursorPagination::VERSION
|
9
|
+
spec.authors = ["Sergey Kukunin"]
|
10
|
+
spec.email = ["sergey.kukunin@gmail.com"]
|
11
|
+
spec.description = %q{ActiveRecord plugin for cursor based pagination. Uses some column and rpp (results per page) to paginate your content. The main advantage against traditional pagination (limmit and offset), that the one URL on specific page will contain the data set, despite the newly added entities. It may be useful on the projects, where new entities are added often, and specific page now isn't specific page tomorrow.}
|
12
|
+
spec.summary = %q{ActiveRecord plugin for cursor based pagination}
|
13
|
+
spec.homepage = "https://github.com/Kukunin/cursor_pagination"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.add_dependency "activerecord", ['>= 3.0.0']
|
17
|
+
spec.add_development_dependency "rspec-rails"
|
18
|
+
spec.add_development_dependency "sqlite3-ruby"
|
19
|
+
spec.add_development_dependency "database_cleaner", ['< 1.1.1']
|
20
|
+
|
21
|
+
spec.files = `git ls-files`.split($/)
|
22
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
23
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module CursorPagination
|
2
|
+
module ActionViewHelper
|
3
|
+
def next_cursor_link(scope, name, params = {}, options = {}, &block)
|
4
|
+
url = next_cursor_url(scope, params)
|
5
|
+
link_to_unless url.nil?, name, url, options.reverse_merge(:rel => 'next') do
|
6
|
+
block.call if block
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def next_cursor_url(scope, params = {})
|
11
|
+
url_for(params.merge(cursor: scope.next_cursor)) unless scope.last_page?
|
12
|
+
end
|
13
|
+
|
14
|
+
def previous_cursor_link(scope, name, params = {}, options = {}, &block)
|
15
|
+
url = previous_cursor_url(scope, params)
|
16
|
+
link_to_unless url.nil?, name, url, options.reverse_merge(:rel => 'previous') do
|
17
|
+
block.call if block
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def previous_cursor_url(scope, params = {})
|
22
|
+
url_for(params.merge(cursor: scope.previous_cursor)) unless scope.first_page?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'cursor_pagination/active_record_model_extension'
|
3
|
+
|
4
|
+
module CursorPagination
|
5
|
+
module ActiveRecordExtension
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
# Future subclasses will pick up the model extension
|
10
|
+
class << self
|
11
|
+
def inherited_with_cursor_pagination(kls) #:nodoc:
|
12
|
+
inherited_without_cursor_pagination kls
|
13
|
+
kls.send(:include, CursorPagination::ActiveRecordModelExtension) if kls.superclass == ActiveRecord::Base
|
14
|
+
end
|
15
|
+
alias_method_chain :inherited, :cursor_pagination
|
16
|
+
end
|
17
|
+
|
18
|
+
# Existing subclasses pick up the model extension as well
|
19
|
+
self.descendants.each do |kls|
|
20
|
+
kls.send(:include, CursorPagination::ActiveRecordModelExtension) if kls.superclass == ActiveRecord::Base
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'cursor_pagination/page_scope_methods'
|
2
|
+
|
3
|
+
module CursorPagination
|
4
|
+
module ActiveRecordModelExtension
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
def self.current_cursor
|
9
|
+
@current_cursor
|
10
|
+
end
|
11
|
+
|
12
|
+
def self._origin_scope
|
13
|
+
@origin_scope
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.cursor_options
|
17
|
+
@cursor_options
|
18
|
+
end
|
19
|
+
|
20
|
+
scope :cursor, Proc.new { |cursor, options|
|
21
|
+
options = { column: :id, reverse: false }.merge(options || {})
|
22
|
+
@current_cursor = cursor
|
23
|
+
@origin_scope = self.all
|
24
|
+
@cursor_options = options
|
25
|
+
|
26
|
+
scope = @origin_scope
|
27
|
+
scope = scope.where("#{options[:column]} #{options[:reverse] ? '<' : '>'} ?", cursor) if cursor
|
28
|
+
scope.limit(25)
|
29
|
+
} do
|
30
|
+
include CursorPagination::PageScopeMethods
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module CursorPagination
|
2
|
+
module PageScopeMethods
|
3
|
+
def per(num)
|
4
|
+
limit(num)
|
5
|
+
end
|
6
|
+
|
7
|
+
def first_page?
|
8
|
+
previous_cursor == -1
|
9
|
+
end
|
10
|
+
|
11
|
+
def last_page?
|
12
|
+
next_cursor == -1
|
13
|
+
end
|
14
|
+
|
15
|
+
def previous_cursor
|
16
|
+
options = cursor_options
|
17
|
+
result = _origin_scope.where("#{options[:column]} #{options[:reverse] ? '>' : '<'}= ?", current_cursor).limit(limit_value+1).to_a
|
18
|
+
case result.size
|
19
|
+
when limit_value+1
|
20
|
+
result.first.send(options[:column])
|
21
|
+
when 0
|
22
|
+
-1 #no previous page
|
23
|
+
else
|
24
|
+
nil #first page, incomplete
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def next_cursor
|
30
|
+
return -1 if last.nil?
|
31
|
+
# try to get something after last cursor
|
32
|
+
cursor = last.send(cursor_options[:column])
|
33
|
+
_origin_scope.cursor(cursor, cursor_options).per(1).count.zero? ? -1 : cursor
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# models
|
2
|
+
class Entity < ActiveRecord::Base
|
3
|
+
|
4
|
+
end
|
5
|
+
|
6
|
+
#migrations
|
7
|
+
class CreateAllTables < ActiveRecord::Migration
|
8
|
+
def self.up
|
9
|
+
create_table :entities do |t|
|
10
|
+
t.integer :custom
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
ActiveRecord::Migration.verbose = false
|
17
|
+
CreateAllTables.up
|
@@ -0,0 +1,30 @@
|
|
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
|
+
|
7
|
+
# config
|
8
|
+
app = Class.new(Rails::Application)
|
9
|
+
app.config.secret_token = 'f47a93ac1ff6843777fed92f966d61dc'
|
10
|
+
app.config.session_store :cookie_store, :key => '_myapp_session'
|
11
|
+
app.config.active_support.deprecation = :log
|
12
|
+
app.config.eager_load = false
|
13
|
+
# Rais.root
|
14
|
+
app.config.root = File.dirname(__FILE__)
|
15
|
+
Rails.backtrace_cleaner.remove_silencers!
|
16
|
+
app.initialize!
|
17
|
+
|
18
|
+
# routes
|
19
|
+
app.routes.draw do
|
20
|
+
resources :entities
|
21
|
+
end
|
22
|
+
|
23
|
+
#models
|
24
|
+
require 'fake_app/active_record/models' if defined? ActiveRecord
|
25
|
+
|
26
|
+
# controllers
|
27
|
+
class ApplicationController < ActionController::Base; end
|
28
|
+
|
29
|
+
# helpers
|
30
|
+
Object.const_set(:ApplicationHelper, Module.new)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CursorPagination::ActionViewHelper do
|
4
|
+
|
5
|
+
include_context "entities"
|
6
|
+
|
7
|
+
describe '#previous_cursor_link' do
|
8
|
+
context 'having previous pages' do
|
9
|
+
context 'the default behaviour' do
|
10
|
+
subject { helper.previous_cursor_link second_page, 'Previous', {:controller => 'entities', :action => 'index'} }
|
11
|
+
it { should be_a String }
|
12
|
+
it { should match(/rel="previous"/) }
|
13
|
+
it { should_not match(/cursor=/) }
|
14
|
+
end
|
15
|
+
context 'the third page' do
|
16
|
+
subject { helper.previous_cursor_link third_page, 'Previous', {:controller => 'entities', :action => 'index'} }
|
17
|
+
it { should be_a String }
|
18
|
+
it { should match(/rel="previous"/) }
|
19
|
+
it { should match(/cursor=#{first_entity.id}/) }
|
20
|
+
end
|
21
|
+
context 'overriding rel=' do
|
22
|
+
subject { helper.previous_cursor_link second_page, 'Previous', {:controller => 'entities', :action => 'index'}, {:rel => 'external'} }
|
23
|
+
it { should match(/rel="external"/) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
context 'the first page' do
|
27
|
+
subject { helper.previous_cursor_link first_page, 'Previous', {:controller => 'entities', :action => 'index'} }
|
28
|
+
it { should be_nil }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#next_cursor_link' do
|
33
|
+
context 'having more page' do
|
34
|
+
context 'the default behaviour' do
|
35
|
+
subject { helper.next_cursor_link first_page, 'More', {:controller => 'entities', :action => 'index'} }
|
36
|
+
it { should be_a String }
|
37
|
+
it { should match(/rel="next"/) }
|
38
|
+
it { should match(/cursor=#{first_entity.id}/) }
|
39
|
+
end
|
40
|
+
context 'overriding rel=' do
|
41
|
+
subject { helper.next_cursor_link first_page, 'More', {:controller => 'entities', :action => 'index'}, { :rel => 'external' } }
|
42
|
+
it { should match(/rel="external"/) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
context 'the last page' do
|
46
|
+
subject { helper.next_cursor_link last_page, 'More', {:controller => 'entities', :action => 'index'} }
|
47
|
+
it { should be_nil }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Entity do
|
4
|
+
|
5
|
+
include_context "entities"
|
6
|
+
|
7
|
+
specify do
|
8
|
+
first_entity.id.should be < second_entity.id
|
9
|
+
first_entity.custom.should be > second_entity.custom
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#cursor method" do
|
13
|
+
it "returns first entity only" do
|
14
|
+
result = first_page.to_a
|
15
|
+
result.size.should eq 1
|
16
|
+
result.first.should eq first_entity
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns second entity only" do
|
20
|
+
result = second_page.to_a
|
21
|
+
result.size.should eq 1
|
22
|
+
result.first.should eq second_entity
|
23
|
+
end
|
24
|
+
|
25
|
+
it "support different orders" do
|
26
|
+
result = Entity.order('id DESC').cursor(second_entity.id, reverse: true).per(1).to_a
|
27
|
+
result.size.should eq 1
|
28
|
+
result.first.should eq first_entity
|
29
|
+
end
|
30
|
+
|
31
|
+
it "support different columns" do
|
32
|
+
result = Entity.cursor(second_entity.id, column: :custom).per(1).to_a
|
33
|
+
result.size.should eq 1
|
34
|
+
result.first.should eq first_entity
|
35
|
+
end
|
36
|
+
|
37
|
+
context "without #per method" do
|
38
|
+
before do
|
39
|
+
25.times { Entity.create! }
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns all Entities" do
|
43
|
+
result = Entity.cursor(nil).to_a
|
44
|
+
result.size.should eq 25
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "cursor methods" do
|
50
|
+
# nil is valid cursor too, it means first page
|
51
|
+
# -1 means unavailable cursor (last or first page)
|
52
|
+
describe "#next_cursor" do
|
53
|
+
##Default settings
|
54
|
+
specify { Entity.cursor(nil).per(10).next_cursor.should eq -1 }
|
55
|
+
specify { Entity.cursor(nil).per(10).should be_last_page }
|
56
|
+
specify { first_page.next_cursor.should eq first_entity.id}
|
57
|
+
specify { first_page.should_not be_last_page}
|
58
|
+
specify { last_page.next_cursor.should eq -1 }
|
59
|
+
|
60
|
+
##Reverse order
|
61
|
+
specify { Entity.order('id DESC').cursor(nil, reverse: true).per(1).next_cursor.should eq last_entity.id }
|
62
|
+
specify { Entity.order('id DESC').cursor(second_entity.id, reverse: true).per(1).next_cursor.should eq -1 }
|
63
|
+
|
64
|
+
##With custom column
|
65
|
+
specify { Entity.order('custom ASC').cursor(second_entity.custom, column: :custom).per(1).next_cursor.should eq -1 }
|
66
|
+
specify { Entity.order('custom ASC').cursor(third_entity.custom, column: :custom).per(1).next_cursor.should eq second_entity.custom }
|
67
|
+
specify { Entity.order('custom ASC').cursor(nil, column: :custom).per(1).next_cursor.should eq last_entity.custom }
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#previous_cursor" do
|
71
|
+
##Default settings
|
72
|
+
#no previous page
|
73
|
+
specify { Entity.cursor(nil).previous_cursor.should eq -1 }
|
74
|
+
specify { Entity.cursor(nil).should be_first_page }
|
75
|
+
#not full previous page
|
76
|
+
specify { Entity.cursor(first_entity.id).previous_cursor.should be_nil }
|
77
|
+
specify { Entity.cursor(first_entity.id).should_not be_first_page }
|
78
|
+
#full previous page
|
79
|
+
specify { third_page.previous_cursor.should eq first_entity.id }
|
80
|
+
specify { third_page.should_not be_first_page }
|
81
|
+
|
82
|
+
##Reverse order
|
83
|
+
specify { Entity.order('id DESC').cursor(nil, reverse: true).previous_cursor.should eq -1 }
|
84
|
+
specify { Entity.order('id DESC').cursor(last_entity.id, reverse: true).previous_cursor.should be_nil }
|
85
|
+
specify { Entity.order('id DESC').cursor(third_entity.id, reverse: true).per(1).previous_cursor.should eq last_entity.id }
|
86
|
+
|
87
|
+
##With custom column
|
88
|
+
specify { Entity.order('custom ASC').cursor(nil, column: :custom).previous_cursor.should eq -1 }
|
89
|
+
specify { Entity.order('custom ASC').cursor(last_entity.custom, column: :custom).previous_cursor.should be_nil }
|
90
|
+
specify { Entity.order('custom ASC').cursor(third_entity.custom, column: :custom).per(1).previous_cursor.should eq last_entity.custom }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
require 'active_record'
|
6
|
+
require 'cursor_pagination'
|
7
|
+
|
8
|
+
require 'fake_app/rails'
|
9
|
+
require 'rspec/rails'
|
10
|
+
|
11
|
+
require 'database_cleaner'
|
12
|
+
|
13
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
14
|
+
#
|
15
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
18
|
+
config.run_all_when_everything_filtered = true
|
19
|
+
config.filter_run :focus
|
20
|
+
|
21
|
+
# Run specs in random order to surface order dependencies. If you find an
|
22
|
+
# order dependency and want to debug it, you can fix the order by providing
|
23
|
+
# the seed, which is printed after each run.
|
24
|
+
# --seed 1234
|
25
|
+
config.order = 'random'
|
26
|
+
|
27
|
+
config.before(:suite) do
|
28
|
+
DatabaseCleaner.strategy = :transaction
|
29
|
+
DatabaseCleaner.clean_with(:truncation)
|
30
|
+
end
|
31
|
+
|
32
|
+
config.before(:each) do
|
33
|
+
DatabaseCleaner.start
|
34
|
+
end
|
35
|
+
|
36
|
+
config.after(:each) do
|
37
|
+
DatabaseCleaner.clean
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
shared_context "entities" do
|
2
|
+
before do
|
3
|
+
4.times { |n| Entity.create! custom: (4 - n)}
|
4
|
+
end
|
5
|
+
|
6
|
+
let(:entities) { Entity.all.to_a }
|
7
|
+
let(:first_entity) { entities.first }
|
8
|
+
let(:second_entity) { entities[1] }
|
9
|
+
let(:third_entity) { entities[2] }
|
10
|
+
let(:last_entity) { entities[3] }
|
11
|
+
|
12
|
+
let(:first_page) { Entity.cursor(nil).per(1) }
|
13
|
+
let(:second_page) { Entity.cursor(first_entity.id).per(1) }
|
14
|
+
let(:third_page) { Entity.cursor(second_entity.id).per(1) }
|
15
|
+
let(:last_page) { Entity.cursor(third_entity.id).per(1) }
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cursor_pagination
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sergey Kukunin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: database_cleaner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - <
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - <
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.1.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: ActiveRecord plugin for cursor based pagination. Uses some column and
|
98
|
+
rpp (results per page) to paginate your content. The main advantage against traditional
|
99
|
+
pagination (limmit and offset), that the one URL on specific page will contain the
|
100
|
+
data set, despite the newly added entities. It may be useful on the projects, where
|
101
|
+
new entities are added often, and specific page now isn't specific page tomorrow.
|
102
|
+
email:
|
103
|
+
- sergey.kukunin@gmail.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- .gitignore
|
109
|
+
- .rspec
|
110
|
+
- Gemfile
|
111
|
+
- LICENSE.txt
|
112
|
+
- README.md
|
113
|
+
- Rakefile
|
114
|
+
- app/helpers/cursor_pagination_helper.rb
|
115
|
+
- cursor_pagination.gemspec
|
116
|
+
- lib/cursor_pagination.rb
|
117
|
+
- lib/cursor_pagination/action_view_helper.rb
|
118
|
+
- lib/cursor_pagination/active_record_extension.rb
|
119
|
+
- lib/cursor_pagination/active_record_model_extension.rb
|
120
|
+
- lib/cursor_pagination/page_scope_methods.rb
|
121
|
+
- lib/cursor_pagination/version.rb
|
122
|
+
- spec/fake_app/active_record/config.rb
|
123
|
+
- spec/fake_app/active_record/models.rb
|
124
|
+
- spec/fake_app/rails.rb
|
125
|
+
- spec/helpers/cursor_pagination_helper_spec.rb
|
126
|
+
- spec/models/entity_spec.rb
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
- spec/support/spec_shared.rb
|
129
|
+
homepage: https://github.com/Kukunin/cursor_pagination
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - '>='
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
requirements: []
|
148
|
+
rubyforge_project:
|
149
|
+
rubygems_version: 2.0.3
|
150
|
+
signing_key:
|
151
|
+
specification_version: 4
|
152
|
+
summary: ActiveRecord plugin for cursor based pagination
|
153
|
+
test_files:
|
154
|
+
- spec/fake_app/active_record/config.rb
|
155
|
+
- spec/fake_app/active_record/models.rb
|
156
|
+
- spec/fake_app/rails.rb
|
157
|
+
- spec/helpers/cursor_pagination_helper_spec.rb
|
158
|
+
- spec/models/entity_spec.rb
|
159
|
+
- spec/spec_helper.rb
|
160
|
+
- spec/support/spec_shared.rb
|