geared_pagination 0.1 → 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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