geared_pagination 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 86fef30f9fb2e850e8b305a079d05d39007ec6bd
4
- data.tar.gz: 83e91624d0fbd65747604f91628e826f00a131b0
3
+ metadata.gz: f2ef2f3f5dc638b5cd4b6ab2459cbf4e513b85e4
4
+ data.tar.gz: 3ae73c3c52ee2dd1a257daff921cabe401373733
5
5
  SHA512:
6
- metadata.gz: f7e00eb4a7598860c873eb1810ffdba1c8e5a30d625d42b6a7e10f035786e593724b74c8c496089495ada18c992af7b41534041e1b5951f8573932a3d533509e
7
- data.tar.gz: c2feb3068a763034a474fca15cd793b3fdeae695cee4c6f0187c4ea2918a76a24f9a124061407922a3b397137119fdc234413153563ba0ed9f3301af9204238e
6
+ metadata.gz: a66259e261f8f9b98459cbd754039fbfb304fd8c700181b3babb4dca4ceeb83efb2e49dbf1e00d1238ede83f1ddbc9d96812cdf603c5e0644bd2d59969fab21a
7
+ data.tar.gz: ba3b678d00e7615b1c8595ca342bd2d9cd1f4dee75a2e20f5ab93f178caf19b25b6d2abd6af3a4eb26b8dcf798ecee69a21d231fd4637e22a33f3fe9ce1087aa
data/Gemfile CHANGED
@@ -3,4 +3,5 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'rake'
6
- gem 'byebug'
6
+ gem 'byebug'
7
+ gem 'actionpack', '>= 5'
@@ -8,27 +8,56 @@ PATH
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (5.1.0.beta1)
11
+ actionpack (5.0.2)
12
+ actionview (= 5.0.2)
13
+ activesupport (= 5.0.2)
14
+ rack (~> 2.0)
15
+ rack-test (~> 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
+ actionview (5.0.2)
19
+ activesupport (= 5.0.2)
20
+ builder (~> 3.1)
21
+ erubis (~> 2.7.0)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
+ activesupport (5.0.2)
12
25
  concurrent-ruby (~> 1.0, >= 1.0.2)
13
26
  i18n (~> 0.7)
14
27
  minitest (~> 5.1)
15
28
  tzinfo (~> 1.1)
16
- addressable (2.5.0)
29
+ addressable (2.5.1)
17
30
  public_suffix (~> 2.0, >= 2.0.2)
31
+ builder (3.2.3)
18
32
  byebug (9.0.6)
19
- concurrent-ruby (1.0.4)
33
+ concurrent-ruby (1.0.5)
34
+ erubis (2.7.0)
20
35
  i18n (0.8.1)
36
+ loofah (2.0.3)
37
+ nokogiri (>= 1.5.9)
38
+ mini_portile2 (2.1.0)
21
39
  minitest (5.10.1)
40
+ nokogiri (1.7.1)
41
+ mini_portile2 (~> 2.1.0)
22
42
  public_suffix (2.0.5)
43
+ rack (2.0.1)
44
+ rack-test (0.6.3)
45
+ rack (>= 1.0)
46
+ rails-dom-testing (2.0.2)
47
+ activesupport (>= 4.2.0, < 6.0)
48
+ nokogiri (~> 1.6)
49
+ rails-html-sanitizer (1.0.3)
50
+ loofah (~> 2.0)
23
51
  rake (12.0.0)
24
- thread_safe (0.3.5)
25
- tzinfo (1.2.2)
52
+ thread_safe (0.3.6)
53
+ tzinfo (1.2.3)
26
54
  thread_safe (~> 0.1)
27
55
 
28
56
  PLATFORMS
29
57
  ruby
30
58
 
31
59
  DEPENDENCIES
60
+ actionpack (>= 5)
32
61
  bundler (~> 1.12)
33
62
  byebug
34
63
  geared_pagination!
data/README.md CHANGED
@@ -1,30 +1,30 @@
1
1
  # Geared Pagination
2
2
 
3
3
  Most pagination schemes use a fixed page size. Page 1 returns as many elements as page 2. But that's
4
- frequently not the most sensible way to page through a large collection when you care about serving the
4
+ frequently not the most sensible way to page through a large recordset when you care about serving the
5
5
  initial request as quickly as possible. This is particularly the case when using the pagination scheme
6
6
  in combination with an infinite scrolling UI.
7
7
 
8
8
  Geared Pagination allows you to define different ratios. By default, we will return 15 elements on page 1,
9
- 30 on page 2, 50 on page 3, and 100 from page 4 and forward. This has proben to be a very sensible set of
9
+ 30 on page 2, 50 on page 3, and 100 from page 4 and forward. This has proven to be a very sensible set of
10
10
  ratios for much of the Basecamp UIs. But you can of course tweak the ratios, use fewer, or even none at all,
11
- if you certain page calls for a fixed-rate scheme.
11
+ if a certain page calls for a fixed-rate scheme.
12
12
 
13
- On json actions that set a page, we'll also also automatically set Link and X-Total-Count headers for APIs
14
- to be able to page through a collection.
13
+ On JSON actions that set a page, we'll also automatically set Link and X-Total-Count headers for APIs
14
+ to be able to page through a recordset.
15
15
 
16
16
  ## Example
17
17
 
18
18
  ```ruby
19
19
  class MessagesController < ApplicationController
20
20
  def index
21
- @page = current_page_from Message.order(created_at: :desc)
21
+ set_page_and_extract_portion_from Message.order(created_at: :desc)
22
22
  end
23
23
  end
24
24
 
25
25
  # app/views/messages/index.html.erb
26
26
 
27
- Showing page <%= @page.number %> of <%= @page.collection.page_count %> (<%= @page.collection.record_count %> total messages):
27
+ Showing page <%= @page.number %> of <%= @page.recordset.page_count %> (<%= @page.recordset.records_count %> total messages):
28
28
 
29
29
  <%= render @page.records %>
30
30
 
@@ -36,5 +36,27 @@ Showing page <%= @page.number %> of <%= @page.collection.page_count %> (<%= @pag
36
36
 
37
37
  ```
38
38
 
39
+
40
+ ## Caching
41
+
42
+ To account for the current page in fragment caches, include the `@page` directly.
43
+ That includes the current page number and gear ratios.
44
+
45
+ Fragment caching a message's comments:
46
+ ```ruby
47
+ <% cache [ @message, @page ] do %>
48
+ <%= render @page.records %>
49
+ <% end %>
50
+ ```
51
+
52
+ NOTE: The page does not include cache keys for all the records. That would require loading all the records,
53
+ defeating the purpose of using the cache. Use a parent record, like a message that's touched when
54
+ new comments are posted, as the cache key instead.
55
+
56
+ ## ETags
57
+
58
+ When a controller action sets an ETag and uses geared pagination, the current page and gear ratios are
59
+ automatically included in the ETag.
60
+
39
61
  ## License
40
62
  Geared Pagination is released under the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'geared_pagination'
3
- s.version = '0.1'
3
+ s.version = '0.2'
4
4
  s.authors = 'David Heinemeier Hansson'
5
5
  s.email = 'david@basecamp.com'
6
6
  s.summary = 'Paginate Active Record sets at variable speeds'
@@ -7,6 +7,7 @@ module GearedPagination
7
7
 
8
8
  included do
9
9
  after_action :set_paginated_headers
10
+ etag { @page if geared_page? }
10
11
  end
11
12
 
12
13
  private
@@ -20,7 +21,11 @@ module GearedPagination
20
21
  end
21
22
 
22
23
  def set_paginated_headers
23
- GearedPagination::Headers.new(page: @page, controller: self).apply if @page.is_a?(GearedPagination::Page)
24
+ GearedPagination::Headers.new(page: @page, controller: self).apply if geared_page?
25
+ end
26
+
27
+ def geared_page?
28
+ @page.is_a? GearedPagination::Page
24
29
  end
25
30
 
26
31
  def current_page_param
@@ -13,7 +13,7 @@ module GearedPagination
13
13
  private
14
14
  def headers
15
15
  Hash.new.tap do |h|
16
- h["X-Total-Count"] = @page.collection.records_count.to_s
16
+ h["X-Total-Count"] = @page.recordset.records_count.to_s
17
17
  h["Link"] = next_page_link_header unless @page.last?
18
18
  end
19
19
  end
@@ -2,15 +2,15 @@ require 'geared_pagination/portion'
2
2
 
3
3
  module GearedPagination
4
4
  class Page
5
- attr_reader :number, :collection
5
+ attr_reader :number, :recordset
6
6
 
7
7
  def initialize(number, from:)
8
- @number, @collection = number, from
8
+ @number, @recordset = number, from
9
9
  @portion = GearedPagination::Portion.new(page_number: number, per_page: from.ratios)
10
10
  end
11
11
 
12
12
  def records
13
- @records ||= @portion.from(collection.records)
13
+ @records ||= @portion.from(recordset.records)
14
14
  end
15
15
 
16
16
 
@@ -28,16 +28,21 @@ module GearedPagination
28
28
  end
29
29
 
30
30
  def only?
31
- collection.page_count == 1
31
+ recordset.page_count == 1
32
32
  end
33
33
 
34
34
  def last?
35
- number == collection.page_count
35
+ number == recordset.page_count
36
36
  end
37
37
 
38
38
 
39
39
  def next_number
40
40
  number + 1
41
41
  end
42
+
43
+
44
+ def cache_key
45
+ "page/#{@portion.cache_key}"
46
+ end
42
47
  end
43
48
  end
@@ -19,5 +19,10 @@ module GearedPagination
19
19
  def offset
20
20
  (page_number - 1).times.sum { |index| ratios[index + 1] }
21
21
  end
22
+
23
+
24
+ def cache_key
25
+ "#{page_number}:#{ratios.cache_key}"
26
+ end
22
27
  end
23
28
  end
@@ -2,7 +2,7 @@ require 'rails/railtie'
2
2
  require 'geared_pagination/controller'
3
3
 
4
4
  class GearedPagination::Engine < ::Rails::Engine
5
- initializer :webpacker do |app|
5
+ initializer :geared_pagination do |app|
6
6
  ActiveSupport.on_load :action_controller do
7
7
  ActionController::Base.send :include, GearedPagination::Controller
8
8
  end
@@ -9,5 +9,9 @@ module GearedPagination
9
9
  def [](page_number)
10
10
  @ratios[page_number - 1] || @ratios.last
11
11
  end
12
+
13
+ def cache_key
14
+ @ratios.join('-')
15
+ end
12
16
  end
13
17
  end
@@ -24,7 +24,7 @@ module GearedPagination
24
24
  residual = residual - ratios[count]
25
25
  end
26
26
 
27
- count
27
+ count > 0 ? count : 1
28
28
  end
29
29
  end
30
30
 
@@ -0,0 +1,66 @@
1
+ require 'test_helper'
2
+ require 'geared_pagination'
3
+ require 'geared_pagination/controller'
4
+ require 'active_support/all'
5
+ require 'action_controller'
6
+
7
+ class RecordingsController < ActionController::Base
8
+ include GearedPagination::Controller
9
+
10
+ def index
11
+ set_page_and_extract_portion_from Recording.all, per_page: params[:per_page]
12
+ render json: @page.records if stale? etag: "placeholder"
13
+ end
14
+
15
+ def unpaged
16
+ @page = "not a geared pagination page"
17
+ head :ok if stale? etag: "placeholder"
18
+ end
19
+ end
20
+
21
+ class GearedPagination::ControllerTest < ActionController::TestCase
22
+ tests RecordingsController
23
+
24
+ setup do
25
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
26
+ r.draw do
27
+ resources :recordings do
28
+ get :unpaged, on: :collection
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ test "ETag includes the current page and gearing" do
35
+ get :index, params: { per_page: [ 1, 2 ] }
36
+ assert_equal etag_for("placeholder", "page/1:1-2"), response.etag
37
+ etag_before_gearing_change = response.etag
38
+
39
+ get :index, params: { page: 1, per_page: [ 1, 2 ] }
40
+ assert_equal etag_before_gearing_change, response.etag
41
+
42
+ get :index, params: { page: 1, per_page: [ 1, 3 ] }
43
+ assert_not_equal etag_before_gearing_change, response.etag
44
+ end
45
+
46
+ test "ETag is ignored when @page is not a geared page" do
47
+ get :unpaged
48
+ assert_equal etag_for("placeholder"), response.etag
49
+ end
50
+
51
+ test "Link headers on JSON requests" do
52
+ get :index, format: 'json'
53
+ assert_equal "120", response.headers["X-Total-Count"]
54
+ assert_equal '<http://test.host/recordings.json?page=2>; rel="next"', response.headers["Link"]
55
+ end
56
+
57
+ test "no Link headers on non-JSON requests" do
58
+ get :index
59
+ assert_nil response.headers["Link"]
60
+ end
61
+
62
+ private
63
+ def etag_for(*keys)
64
+ %(W/"#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(keys))}")
65
+ end
66
+ end
@@ -8,40 +8,40 @@ class GearedPagination::HeadersTest < ActiveSupport::TestCase
8
8
  Controller = Struct.new(:request, :headers)
9
9
 
10
10
  setup do
11
- @controller_serving_json = Controller.new(Request.new("http://example.com/collection.json", "json".inquiry), {})
12
- @controller_serving_html = Controller.new(Request.new("http://example.com/collection", "html".inquiry), {})
11
+ @controller_serving_json = Controller.new(Request.new("http://example.com/recordset.json", "json".inquiry), {})
12
+ @controller_serving_html = Controller.new(Request.new("http://example.com/recordset", "html".inquiry), {})
13
13
 
14
- @single_page_collection = GearedPagination::Recordset.new(Recording.all, per_page: 1000)
15
- @many_page_collection = GearedPagination::Recordset.new(Recording.all, per_page: 1)
14
+ @single_page_recordset = GearedPagination::Recordset.new(Recording.all, per_page: 1000)
15
+ @many_page_recordset = GearedPagination::Recordset.new(Recording.all, per_page: 1)
16
16
  end
17
17
 
18
18
  test "total count" do
19
- GearedPagination::Headers.new(page: @many_page_collection.page(1), controller: @controller_serving_json).apply
19
+ GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_json).apply
20
20
  assert_equal Recording.all.count.to_s, @controller_serving_json.headers["X-Total-Count"]
21
21
  end
22
22
 
23
23
  test "no link for html requests" do
24
- GearedPagination::Headers.new(page: @many_page_collection.page(1), controller: @controller_serving_html).apply
24
+ GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_html).apply
25
25
  assert @controller_serving_html.headers["Link"].nil?
26
26
  end
27
27
 
28
28
  test "no link for json request with single page" do
29
- GearedPagination::Headers.new(page: @many_page_collection.page(1), controller: @controller_serving_html).apply
29
+ GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_html).apply
30
30
  assert @controller_serving_html.headers["Link"].nil?
31
31
  end
32
32
 
33
33
  test "links for json request with multiple pages" do
34
- GearedPagination::Headers.new(page: @many_page_collection.page(1), controller: @controller_serving_json).apply
35
- assert_equal '<http://example.com/collection.json?page=2>; rel="next"',
34
+ GearedPagination::Headers.new(page: @many_page_recordset.page(1), controller: @controller_serving_json).apply
35
+ assert_equal '<http://example.com/recordset.json?page=2>; rel="next"',
36
36
  @controller_serving_json.headers["Link"]
37
37
 
38
- GearedPagination::Headers.new(page: @many_page_collection.page(2), controller: @controller_serving_json).apply
39
- assert_equal '<http://example.com/collection.json?page=3>; rel="next"',
38
+ GearedPagination::Headers.new(page: @many_page_recordset.page(2), controller: @controller_serving_json).apply
39
+ assert_equal '<http://example.com/recordset.json?page=3>; rel="next"',
40
40
  @controller_serving_json.headers["Link"]
41
41
  end
42
42
 
43
43
  test "no link for json request with multiple pages on last page" do
44
- GearedPagination::Headers.new(page: @many_page_collection.page(Recording.all.count), controller: @controller_serving_json).apply
44
+ GearedPagination::Headers.new(page: @many_page_recordset.page(Recording.all.count), controller: @controller_serving_json).apply
45
45
  assert @controller_serving_json.headers["Link"].nil?
46
46
  end
47
47
  end
@@ -20,4 +20,23 @@ class GearedPagination::PageTest < ActiveSupport::TestCase
20
20
  test "next_number" do
21
21
  assert_equal 2, GearedPagination::Recordset.new(Recording.all, per_page: 1000).page(1).next_number
22
22
  end
23
+
24
+ test "with empty recordset" do
25
+ page_for_empty_set = GearedPagination::Recordset.new(Recordings.new([]), per_page: 1000).page(1)
26
+
27
+ assert page_for_empty_set.first?
28
+ assert page_for_empty_set.only?
29
+ assert page_for_empty_set.last?
30
+ end
31
+
32
+ test "cache key changes according to current page and gearing" do
33
+ assert_equal 'page/2:3', cache_key(page: 2, per_page: 3)
34
+ assert_equal 'page/2:1-3', cache_key(page: 2, per_page: [ 1, 3 ])
35
+ assert_equal 'page/2:2-3', cache_key(page: 2, per_page: [ 2, 3 ])
36
+ end
37
+
38
+ private
39
+ def cache_key(page:, per_page:)
40
+ GearedPagination::Recordset.new(Recording.all, per_page: per_page).page(page).cache_key
41
+ end
23
42
  end
@@ -13,4 +13,15 @@ class GearedPagination::PortionTest < ActiveSupport::TestCase
13
13
  assert_equal GearedPagination::Ratios::DEFAULTS.first, GearedPagination::Portion.new(page_number: 1).limit
14
14
  assert_equal GearedPagination::Ratios::DEFAULTS.second, GearedPagination::Portion.new(page_number: 2).limit
15
15
  end
16
+
17
+ test "cache key changes according to current page and gearing" do
18
+ assert_equal '2:3', cache_key(page: 2, per_page: 3)
19
+ assert_equal '2:1-3', cache_key(page: 2, per_page: [ 1, 3 ])
20
+ assert_equal '2:2-3', cache_key(page: 2, per_page: [ 2, 3 ])
21
+ end
22
+
23
+ private
24
+ def cache_key(page:, per_page:)
25
+ GearedPagination::Portion.new(page_number: page, per_page: GearedPagination::Ratios.new(per_page)).cache_key
26
+ end
16
27
  end
@@ -24,4 +24,8 @@ class GearedPagination::RatiosTest < ActiveSupport::TestCase
24
24
  assert_equal GearedPagination::Ratios::DEFAULTS.first, limits[1]
25
25
  assert_equal GearedPagination::Ratios::DEFAULTS.last, limits[99]
26
26
  end
27
+
28
+ test "cache key" do
29
+ assert_equal "1-2-3", GearedPagination::Ratios.new([ 1, 2, 3 ]).cache_key
30
+ end
27
31
  end
@@ -3,26 +3,26 @@ require 'geared_pagination/recordset'
3
3
 
4
4
  class GearedPagination::RecordsetTest < ActiveSupport::TestCase
5
5
  test "single limit pagination" do
6
- collection = GearedPagination::Recordset.new(Recording.all, per_page: 10)
6
+ recordset = GearedPagination::Recordset.new(Recording.all, per_page: 10)
7
7
 
8
- assert_equal 10, collection.page(1).records.size
9
- assert_equal 10, collection.page(2).records.size
8
+ assert_equal 10, recordset.page(1).records.size
9
+ assert_equal 10, recordset.page(2).records.size
10
10
  end
11
11
 
12
12
  test "variable limit pagination" do
13
- collection = GearedPagination::Recordset.new(Recording.all, per_page: [ 10, 15, 20 ])
13
+ recordset = GearedPagination::Recordset.new(Recording.all, per_page: [ 10, 15, 20 ])
14
14
 
15
- assert_equal 10, collection.page(1).records.size
16
- assert collection.page(1).records.include?(Recording.all[0])
15
+ assert_equal 10, recordset.page(1).records.size
16
+ assert recordset.page(1).records.include?(Recording.all[0])
17
17
 
18
- assert_equal 15, collection.page(2).records.size
19
- assert collection.page(2).records.include?(Recording.all[11])
18
+ assert_equal 15, recordset.page(2).records.size
19
+ assert recordset.page(2).records.include?(Recording.all[11])
20
20
 
21
- assert_equal 20, collection.page(3).records.size
22
- assert collection.page(3).records.include?(Recording.all[26])
21
+ assert_equal 20, recordset.page(3).records.size
22
+ assert recordset.page(3).records.include?(Recording.all[26])
23
23
 
24
- assert_equal 20, collection.page(4).records.size
25
- assert collection.page(4).records.include?(Recording.all[46])
24
+ assert_equal 20, recordset.page(4).records.size
25
+ assert recordset.page(4).records.include?(Recording.all[46])
26
26
  end
27
27
 
28
28
  test "page count" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geared_pagination
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-30 00:00:00.000000000 Z
11
+ date: 2017-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -72,6 +72,7 @@ files:
72
72
  - lib/geared_pagination/railtie.rb
73
73
  - lib/geared_pagination/ratios.rb
74
74
  - lib/geared_pagination/recordset.rb
75
+ - test/controller_test.rb
75
76
  - test/headers_test.rb
76
77
  - test/page_test.rb
77
78
  - test/portion_test.rb
@@ -104,6 +105,7 @@ signing_key:
104
105
  specification_version: 4
105
106
  summary: Paginate Active Record sets at variable speeds
106
107
  test_files:
108
+ - test/controller_test.rb
107
109
  - test/headers_test.rb
108
110
  - test/page_test.rb
109
111
  - test/portion_test.rb