Skip to content

Commit

Permalink
added out_of_range extra (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Jul 13, 2018
1 parent 37119ee commit c52db2a
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 14 deletions.
10 changes: 8 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require "rubocop/rake_task"
Rake::TestTask.new(:test_common) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList.new.include("test/**/*_test.rb").exclude('test/**/i18n_test.rb', 'test/**/items_test.rb')
t.test_files = FileList.new.include("test/**/*_test.rb").exclude('test/**/i18n_test.rb', 'test/**/items_test.rb', 'test/**/out_of_range_test.rb')
end

Rake::TestTask.new(:test_extra_i18n) do |t|
Expand All @@ -25,7 +25,13 @@ Rake::TestTask.new(:test_extra_items) do |t|
t.test_files = FileList['test/**/items_test.rb']
end

task :test => [:test_common, :test_extra_items, :test_extra_i18n]
Rake::TestTask.new(:test_extra_out_of_range) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList['test/**/out_of_range_test.rb']
end

task :test => [:test_common, :test_extra_items, :test_extra_i18n, :test_extra_out_of_range ]

RuboCop::RakeTask.new(:rubocop) do |t|
t.options = `git ls-files -z`.split("\x0") # limit rubocop to the files in the repo
Expand Down
3 changes: 2 additions & 1 deletion docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
<a href="{{ site.baseurl }}/extras"><p {% if page.title == 'Extras' %}id="active"{% endif %} >Extras</p></a>
<a href="{{ site.baseurl }}/extras/array"><p class="indent1" {% if page.title == 'Array' %}id="active"{% endif %} >Array</p></a>
<a href="{{ site.baseurl }}/extras/bootstrap"><p class="indent1" {% if page.title == 'Bootstrap' %}id="active"{% endif %} >Bootstrap</p></a>
<a href="{{ site.baseurl }}/extras/bulma"><p class="indent1" {% if page.title == 'Bootstrap' %}id="active"{% endif %} >Bulma</p></a>
<a href="{{ site.baseurl }}/extras/bulma"><p class="indent1" {% if page.title == 'Bulma' %}id="active"{% endif %} >Bulma</p></a>
<a href="{{ site.baseurl }}/extras/compact"><p class="indent1" {% if page.title == 'Compact' %}id="active"{% endif %} >Compact</p></a>
<a href="{{ site.baseurl }}/extras/i18n"><p class="indent1" {% if page.title == 'I18n' %}id="active"{% endif %} >I18n</p></a>
<a href="{{ site.baseurl }}/extras/items"><p class="indent1" {% if page.title == 'Items' %}id="active"{% endif %} >Items</p></a>
<a href="{{ site.baseurl }}/extras/out_of_range"><p class="indent1" {% if page.title == 'Out Of Range' %}id="active"{% endif %} >Out Of Range</p></a>
<a href="{{ site.baseurl }}/extras/responsive"><p class="indent1" {% if page.title == 'Responsive' %}id="active"{% endif %} >Responsive</p></a>
<a href="{{ site.baseurl }}/migration-tips"><p {% if page.title == 'Migration Tips' %}id="active"{% endif %} >Migration Tips</p></a>
<p id="gitter-support"><a href="https://gitter.im/ruby-pagy/Lobby" rel="nofollow" target="_blank">&gt; Chat Support on Gitter &lt;</a></p>
Expand Down
19 changes: 10 additions & 9 deletions docs/extras.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ title: Extras

Pagy comes with a few optional extensions/extras:

| Extra | Description | Links |
| ------------ | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `array` | Paginate arrays efficiently avoiding expensive array-wrapping and without overriding | [array.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/array.rb), [documentation](extras/array.md) |
| `bootstrap` | Nav helper and templates for Bootstrap pagination | [bootstrap.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bootstrap.rb), [documentation](extras/bootstrap.md) |
| `bulma` | Nav helper and templates for [Bulma](https://bulma.io) pagination component | [bulma.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bulma.rb), [documentation](extras/bulma.md) |
| `compact` | An alternative UI that combines the pagination with the nav info in a single compact element | [compact.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/compact.rb), [documentation](extras/compact.md) |
| `i18n` | Use the `I18n` gem instead of the pagy implementation | [i18n.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/i81n.rb), [documentation](extras/i18n.md) |
| `items` | Allow the client to request a custom number of items per page with a ready to use selector UI | [items.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/items.rb), [documentation](extras/items.md) |
| `responsive` | On resize, the number of page links will adapt in real-time to the available window or container width | [responsive.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/responsive.rb), [documentation](extras/responsive.md) |
| Extra | Description | Links |
| -------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `array` | Paginate arrays efficiently avoiding expensive array-wrapping and without overriding | [array.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/array.rb), [documentation](extras/array.md) |
| `bootstrap` | Nav helper and templates for Bootstrap pagination | [bootstrap.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bootstrap.rb), [documentation](extras/bootstrap.md) |
| `bulma` | Nav helper and templates for [Bulma](https://bulma.io) pagination component | [bulma.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/bulma.rb), [documentation](extras/bulma.md) |
| `compact` | An alternative UI that combines the pagination with the nav info in a single compact element | [compact.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/compact.rb), [documentation](extras/compact.md) |
| `i18n` | Use the `I18n` gem instead of the pagy implementation | [i18n.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/i81n.rb), [documentation](extras/i18n.md) |
| `items` | Allow the client to request a custom number of items per page with a ready to use selector UI | [items.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/items.rb), [documentation](extras/items.md) |
| `out_of_range` | Allow for easy handling of out of range pages | [out_of_range.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/out_of_range.rb), [documentation](extras/out_of_range.md) |
| `responsive` | On resize, the number of page links will adapt in real-time to the available window or container width | [responsive.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/responsive.rb), [documentation](extras/responsive.md) |

## Synopsys

Expand Down
96 changes: 96 additions & 0 deletions docs/extras/out_of_range.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
title: Out Of Range
---
# Out Of Range Extra

This extra allows for easy handling of out of range pages. It internally resques from the `Pagy::OutOfRangeError` offering a few different ready to use modes, quite useful for UIs and/or APIs.

## Synopsys

See [extras](../extras.md) for general usage info.

In the Pagy initializer:

```ruby
require 'pagy/extras/out_of_range'

# default :last_page (other options :empty_page and :exception )
Pagy::VARS[:out_of_range_mode] = :last_page
```

## Files

This extra is composed of the [out_of_range.rb](https://github.com/ddnexus/pagy/blob/master/lib/pagy/extras/out_of_range.rb) file.

## Variables

| Variable | Description | Default |
| -------------------- | ------------------------------------------ | ------------ |
| `:out_of_range_mode` | `:last_page`, `empty_page` or `:exception` | `:last_page` |

As usual, depending on the scope of the customization, you have a couple of options to set the variables:

As a global default:

```ruby
Pagy::VARS[:out_of_range_mode] = :empty_page
```

For a single instance (overriding the global default):

```ruby
pagy(scope, out_of_range_mode: :empty_page)
Pagy.new(count:100, out_of_range_mode: :empty_page)
```

## Modes

These are the modes accepted by the `:out_of_range_mode` variable:

### :last_page

This is the default mode. It is useful in apps with an UI, in order to avoid to redirect to the last page.

Regardless the out of range page requested, Pagy will set the page to the last page and paginate exactly as if the last page has been requested. For example:

```ruby
# no exception passing an out of range page (Default mode :last_page)
pagy = Pagy.newpag(count: 100, page: 100)

pagy.out_of_range? #=> true
pagy.vars[:page] #=> 100 (requested page)
pagy.page #=> 5 (curentlast page)
pagy.last == pagy.page #=> true
```

### :empty_page

This mode will paginate the actual requested page, which - being out of range - is empty. It is useful with APIs, where the client expects an empty set of results in order to stop requesting further pages. For example:

```ruby
pagy = Pagy.new(count: 100, page: 100, out_of_range_mode: :empty_page)

pagy.out_of_range? #=> true
pagy.vars[:page] #=> 100 (requested page)
pagy.page #=> 100 (actual empty page)
pagy.last == pagy.page #=> false
pagy.last #=> 5
pagy.last == pagy.prev #=> true (the prev page is the last page relative to out of range page)
pagy.next #=> nil
pagy.offset #=> 0
pagy.items #=> 0
pagy.from #=> 0
pagy.to #=> 0

pagy.series #=> [1, 2, 3, 4, 5] (no string, so no current page highlighted in the UI)
```

### :exception

This mode raises the `Pagy::OutOfRangeError` as usual, so you can rescue from and do what is needed. It is useful when you need to use your own custom mode even in presence of this extra (which would not raise any error).

## Methods

### out_of_range?

Use this method in order to know if the requested page is out of range. The original requested page is available as `pagy.vars[:page]` (useful when used with the `:last_page` mode, in case you want to give some feedback about the rescue to the user/client).
15 changes: 13 additions & 2 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,19 @@ You can do so by setting the `:item_path` variable to the path to lookup in the
## Handling Pagy::OutOfRangeError exception

Pass an out of range `:page` number and Pagy will raise a `Pagy::OutOfRangeError` exception.
This often happens because users paginate over the end of the record set or records go deleted and a user went to a stale page.
A few options for handling this are:

This often happens because users/clients paginate over the end of the record set or records go deleted and a user went to a stale page.

You can rescue the exception manually or use the [out_of_range extra](extras/out_of_range.md):

### Manual Rescue

A few options for manually handling the error in apps are:

- Do nothing and let the page render a 500
- Rescue and render a 404
- Rescue and redirect to the last know page

```ruby
# in a controller
rescue_from Pagy::OutOfRangeError, with: :redirect_to_last_page
Expand All @@ -400,6 +407,7 @@ A few options for handling this are:
redirect_to url_for(page: e.pagy.last), notice: "Page ##{params[:page]} is out of range. Showing page #{e.pagy.last} instead."
end
```

- Rescue and render a page without results, this can be useful for api responses where clients iterate until they see an empty page
```ruby
results = begin
Expand All @@ -408,3 +416,6 @@ A few options for handling this are:
[]
end
```

### Use the "out_of_range" Extra

4 changes: 4 additions & 0 deletions lib/pagy/extras/initializer_example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
# Pagy::VARS[:items_param] = :items # default
# Pagy::VARS[:max_items] = 100 # default

# Out Of Range: Allow for easy handling of out of range pages
# See https://ddnexus.github.io/pagy/extras/out_of_range
# Pagy::VARS[:out_of_range_mode] = :last_page # default (other options :empty_page and :exception )

# Responsive: On resize, the number of page links will adapt in real-time to the available window or container width
# See https://ddnexus.github.io/pagy/extras/responsive
# require 'pagy/extras/responsive'
Expand Down
29 changes: 29 additions & 0 deletions lib/pagy/extras/out_of_range.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class Pagy

VARS[:out_of_range_mode] = :last_page

def out_of_range?; @out_of_range end

alias :create :initialize

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

how about prepending a module and then calling super ?

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

making the alias create breaks down if there are multiple aliases too, usually something like initialize_without_ out_of_range would be used

This comment has been minimized.

Copy link
@ddnexus

ddnexus Jul 13, 2018

Author Owner

Well, I can rename it, but the problem ov breaking will just move from create to initialize_without_out_of_range :)

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

try the prepend it's neat and makes the intention of the code very clear
initialize_without_out_of_range is already a good step forward since it reveals the intention more and will be understandable in a backtrace too


def initialize(vars)
create(vars)
rescue OutOfRangeError => e
raise e if @vars[:out_of_range_mode] == :exception

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

This should be under the @out_of_range assignment so a rescued errors pagy is out_of_range?

This comment has been minimized.

Copy link
@ddnexus

ddnexus Jul 13, 2018

Author Owner

I thought about this and I decided to put it after because the :exception mode should actually obliterate the extra, but what you say makes sense and doesn't harm anything. I will move it before.

@out_of_range = true
if @vars[:out_of_range_mode] == :last_page
@page = @last # set as last page
elsif @vars[:out_of_range_mode] == :empty_page
@offset = @items = @from = @to = 0 # vars relative to the actual page
@prev = @last # the prev is the last page
define_singleton_method(:series) do |size=@vars[:size]|

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

this is kinda hacky/hard to debug ... would be nicer to override series and check @out_of_range in there

This comment has been minimized.

Copy link
@ddnexus

ddnexus Jul 13, 2018

Author Owner

I was trying to avoid extra conditions... it would be checking @out_of_range && @vars[:out_of_range_mode] == :empty_page... I will think about "hacky" :)

@page = @last # series for last page
super(size).tap do |s| # call original series
s[s.index(@page.to_s)] = @page # string to integer (i.e. no current page)
@page = @vars[:page] # restore the actual page
end
end
end

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

if misconfigured this would get into a weird state, should be else raise ArgumentError, "Unknown mode #{@vars[: out_of_range_mode]}"

This comment has been minimized.

Copy link
@ddnexus

ddnexus Jul 13, 2018

Author Owner

True! Thanks!

end

end
63 changes: 63 additions & 0 deletions test/pagy/extras/out_of_range_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require 'pagy'
require_relative '../../test_helper'
require 'pagy/extras/out_of_range'

SingleCov.covered!

describe Pagy do

let(:vars) {{ page: 100, count: 103, items: 10, size: [3, 2, 2, 3] }}
let(:pagy) {Pagy.new(vars)}

describe "variables" do

it 'has vars defaults' do
Pagy::VARS[:out_of_range_mode].must_equal :last_page
end

end

describe "#out_of_range?" do

it 'must be out_of_range?' do
pagy.must_be :out_of_range?
Pagy.new(vars.merge(page:2)).wont_be :out_of_range?
end

end


describe "#initialize" do

it 'initializes with out of range page' do
pagy.must_be_instance_of Pagy
pagy.page.must_equal pagy.last
pagy.vars[:page].must_equal 100
end

it 'raises OutOfRangeError in :exception mode' do
proc { Pagy.new(vars.merge(out_of_range_mode: :exception)) }.must_raise Pagy::OutOfRangeError
end

it 'works in :empty_page mode' do
pagy = Pagy.new(vars.merge(out_of_range_mode: :empty_page))
pagy.page.must_equal 100
pagy.offset.must_equal 0
pagy.items.must_equal 0
pagy.from.must_equal 0
pagy.to.must_equal 0
pagy.prev.must_equal pagy.last
end

end

describe "#series singleton for :empty_page mode" do
it 'computes series for last page' do

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

the newline mode is inconsistent ... either don't have newlines after describe+before end or do

This comment has been minimized.

Copy link
@grosser

grosser Jul 13, 2018

Contributor

(prefer no newlines since that's more common and consistent with method definitions)

This comment has been minimized.

Copy link
@ddnexus

ddnexus Jul 13, 2018

Author Owner

Thanks

pagy = Pagy.new(vars.merge(out_of_range_mode: :empty_page))
series = pagy.series
series.must_equal [1, 2, 3, :gap, 9, 10, 11]
pagy.page.must_equal 100
end
end

end

2 comments on commit c52db2a

@grosser
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like it solves the issue, thanks!
A PR to discuss instead of a straight up release would have been nice though.

@ddnexus
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impatience :)

Please sign in to comment.