-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pagination Links #1041
Pagination Links #1041
Changes from 1 commit
f7c77c1
b864302
1fe8b06
e040d6f
331218d
acb6545
36c452e
e62a7d6
7be25fe
e0d050d
77a8f66
59ae84b
a41d90c
2c2f948
5031eb9
01eab3b
f85027e
3c3578a
b73ffe2
d50262e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Pagination links will be included in your response automatically as long as the resource is paginated using Kaminari or WillPaginate and if you are using a JSON-API adapter. The others adapters does not have this feature.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,18 @@ | ||
# How to add pagination links | ||
|
||
If you want pagination links in your response, specify it in the `render` | ||
Pagination links will be included in your response automatically as long as the resource is paginated and if you are using a ```JSON-API``` adapter. The others adapters does not have this feature. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But you could use the PaginationLinks builder with the JSON adapter right? like if I wanted to put links in the 'meta' attribute using a subclass of the json adapter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, PaginationLinks only will be included to Ex. render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer class PaginatedSerializer < ActiveModel::Serializer::ArraySerializer
def initialize(object, options={})
meta_key = options[:meta_key] || :meta
options[meta_key] ||= {}
options[meta_key] = {
current_page: object.current_page,
next_page: object.next_page,
prev_page: object.prev_page,
total_pages: object.total_pages,
total_count: object.total_count
}
super(object, options)
end
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Winning! That in the docs? (I'm in a car now)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is not! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Up to you, but I think it could be linked from the CollectionSerializer above https://github.com/rails-api/active_model_serializers/pull/1041/files#diff-04c6e90faac2675aa89e2176d2eec7d8R121 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 yup, this would be good to have it, maybe a new article on the docs 😄 |
||
```ruby | ||
render json: @posts, pagination: true | ||
``` | ||
If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). | ||
|
||
AMS relies on either `Kaminari` or `WillPaginate`. Please install either dependency by adding one of those to your Gemfile. | ||
```ruby | ||
#kaminari example | ||
@posts = Kaminari.paginate_array(Post.all).page(3).per(1) | ||
render json: @posts | ||
|
||
Pagination links will only be included in your response if you are using a ```JSON-API``` adapter, the others adapters doesn't have this feature. | ||
#will_paginate example | ||
@posts = Post.all.paginate(page: 3, per_page: 1) | ||
render json: @posts | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bacarini would you mind updating the will_paginate example to use the 'newer, recommended' syntax that doesn't require the deprecated finders?
Or even
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey, @bf4 tks for the review. I believe that, using
What do you think doing this way? Kaminari examples#array
@posts = Kaminari.paginate_array([1, 2, 3]).page(3).per(1)
render json: @posts
#active_record
@posts = Post.page(3).per(1)
render json: @posts WillPaginate examples#array
@posts = [1,2,3].paginate(page: 3, per_page: 1)
render json: @posts
#active_record
@posts = Post.page(3).per_page(1)
render json: @posts There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point
|
||
|
||
```ruby | ||
ActiveModel::Serializer.config.adapter = :json_api | ||
|
@@ -38,3 +42,5 @@ ex: | |
} | ||
} | ||
``` | ||
|
||
AMS relies on either [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate). Please install either dependency by adding one of those to your Gemfile. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: AMS relies on either Kaminari or WillPaginate for pagination. alternative to consider: AMS pagination relies on a paginated collection with the methods (links left out for ease of me typing :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,9 +25,10 @@ def get_serializer(resource, options = {}) | |
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource#serialize" | ||
options[:adapter] = false | ||
end | ||
if options[:pagination] | ||
options[:original_url] = original_url | ||
options[:query_parameters] = query_parameters | ||
if resource.respond_to?(:current_page) && resource.respond_to?(:total_pages) | ||
options[:pagination] = {} | ||
options[:pagination][:original_url] = original_url | ||
options[:pagination][:query_parameters] = query_parameters | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just thinking about this part of the pagination interface and how someone might want to generate this outside of a controller might we want to instead set these as: options[:pagination][:original_url] ||= original_url
options[:pagination][:query_parameters] ||= query_parameters that way options[:pagination] is still truthy and its options are scoped within it... and is also passed to the adapter as such On the other hand, since original url is really a fallback for Another alternative is to pass I rearranged your code as below while thinking this through. Hope it's helpful. FIRST_PAGE = 1
def options
@options ||=
begin
{
pagination: {
original_url: request.original_url,
query_parameters: request.query_parameters
}
}
end
end
def links
@links ||= options[:links] || {}
end
def build_pagination_links(collection)
pagination_links = {}
return pagination_links if collection.total_pages == FIRST_PAGE
original_url = options[:pagination][:original_url]
url_base = original_url[/\A[^?]+/]
query_parameters = options[:query_parameters].dup || original_url[/\?.*/] || {}
current_links = links
self_link = current_links.fetch(:self) { original_url }
collection_size = collection.size
collection_current_page = collection.current_page
collection_total_pages = collection.total_pages
params = query_parameters.merge!(page: {})
link_template = "#{url_base}?%s"
# only add first/previous page links when not on the first page (i.e. we can go back)
if collection_current_page != FIRST_PAGE
pagination_links[:first] = link_template % params[:page].merge(size: collection_size, number: FIRST_PAGE).to_query
pagination_links[:prev] = link_template % params[:page].merge(size: collection_size, number: (collection_current_page - FIRST_PAGE)).to_query
end
# only add next/last page links when not on the last page (i.e. we can go forward)
if collection_current_page != collection_total_pages
pagination_links[:next] = link_template % params[:page].merge(size: collection_size, number: (collection_current_page + FIRST_PAGE)).to_query
pagination_links[:last] = link_template % params[:page].merge(size: collection_size, number: collection_total_pages).to_query
end
pagination_links
end
links.update(build_pagination_links(paginated_resource)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍 I'm also in favor or building the self link based on this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could we maybe instead pass in a e.g. in the controller, just options.fetch(:context) { options[:context] = request }
ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource| Then in the ArraySerializer class ArraySerializer
def object # Same interface as Serializer. The instance_variable_get(:@resources) in the code is an interface smell
@resource
end
end Then in the JsonApi adapter def serializable_hash(options = nil)
options ||= {}
if serializer.respond_to?(:each)
serializer.each do |s|
# code
end
add_links(options) # add links when we have a collection serializer
end
private
def add_links(options)
links = @hash[:links] ||= {} # no reason for them to be nil or false
@hash[:links].update(JsonApi::PaginationLinks.new(object, links).serializable_hash(options))
end
end Then in the PaginationLinks builder attr_reader :collection
def initialize(collection, links)
@collection = collection
@links = links
end
def serializable_hash(options)
pagination_links = {}
return pagination_links if !collection.respond_to?(:current_page) && !collection.respond_to?(:total_pages)
return pagination_links if collection.total_pages == FIRST_PAGE
context = options.fetch(:context) # not sure what the desired behavior is here. If context (the controller.request) is required, I shouldn't be putting it in 'options'. The idea, though, is that we only need to get the current url etc from the context when pagination. No need to do it in the controller when passing it in, I don't think.
original_url = context.original_url
url_base = original_url[/\A[^?]+/]
query_parameters = context.query_parameters.dup || original_url[/\?.*/].to_query || {} # this line is broken, but it's the general idea...
current_links = @links
self_link = current_links.fetch(:self) { original_url }
collection_size = collection.size
collection_current_page = collection.current_page
collection_total_pages = collection.total_pages
params = query_parameters.merge!(page: {})
link_template = "#{url_base}?%s"
# only add first/previous page links when not on the first page (i.e. we can go back)
if collection_current_page != FIRST_PAGE
pagination_links[:first] = link_template % params[:page].merge(size: collection_size, number: FIRST_PAGE).to_query # confirm that the url this generates has params like `first: "page[size]=1&page[number]=10"`
pagination_links[:prev] = link_template % params[:page].merge(size: collection_size, number: (collection_current_page - FIRST_PAGE)).to_query
end
# only add next/last page links when not on the last page (i.e. we can go forward)
if collection_current_page != collection_total_pages
pagination_links[:next] = link_template % params[:page].merge(size: collection_size, number: (collection_current_page + FIRST_PAGE)).to_query
pagination_links[:last] = link_template % params[:page].merge(size: collection_size, number: collection_total_pages).to_query
end
pagination_links
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey @bf4 , Is the idea to send the whole request through objects? It might be an option if this feature will be approved. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bacarini Yeah, the idea is that since we only need to get the current url etc from the context when paginating, instead of manipulating the request object in the controller, and pass in two pieces of generated data via That make sense? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a bunch wrong in the specifics of the code above, I totally admit. I was more interested in the flow.. but e.g. def serializable_hash(options)
pagination_links = {} is wrong. those options passed in to serializable_hash should be the pagination_links options. The thing that has the context should be passed into the initializer, I think. i.e. # the context is the request object from the controller and it needs to have original_url and query_parameters
def initialize(collection, links, context)
@collection = collection
@links = links
@context = context
end
def serializable_hash(pagination_links = nil)
pagination_links ||= {} |
||
ActiveModel::SerializableResource.serialize(resource, options) do |serializable_resource| | ||
if serializable_resource.serializer? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,15 +27,11 @@ def using_will_paginate | |
end | ||
|
||
def data | ||
{ | ||
data: [{ | ||
id:"2", | ||
type:"profiles", | ||
attributes:{ | ||
name:"Name 2", | ||
description:"Description 2" | ||
} | ||
}] | ||
{ data:[ | ||
{ id:"1", type:"profiles", attributes:{name:"Name 1", description:"Description 1" } }, | ||
{ id:"2", type:"profiles", attributes:{name:"Name 2", description:"Description 2" } }, | ||
{ id:"3", type:"profiles", attributes:{name:"Name 3", description:"Description 3" } } | ||
] | ||
} | ||
end | ||
|
||
|
@@ -56,42 +52,48 @@ def expected_response_without_pagination_links | |
end | ||
|
||
def expected_response_with_pagination_links | ||
data.merge links | ||
{}.tap do |hash| | ||
hash[:data] = [data.values.flatten.second] | ||
hash.merge! links | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. overall, I'm happy with this PR and I think it's ok to merge. I'd like to change this to something like expected_response_without_pagination_links.merge(links) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. which I would submit in a follow-up pr to keep this moving |
||
end | ||
|
||
def expected_response_with_pagination_links_and_additional_params | ||
new_links = links[:links].each_with_object({}) {|(key, value), hash| hash[key] = "#{value}&teste=teste" } | ||
data.merge links: new_links | ||
{}.tap do |hash| | ||
hash[:data] = [data.values.flatten.second] | ||
hash.merge! links: new_links | ||
end | ||
end | ||
|
||
def test_pagination_links_using_kaminari | ||
serializer = ArraySerializer.new(using_kaminari) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #1018 (comment) re: using the SerializableResource interface, tl;dr def load_adapter(paginated_collection, options)
options = options.merge(adapter: :json_api)
ActiveModel::SerializableResource.new(paginated_collection, options)
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done! 👍 😄 |
||
|
||
assert_equal expected_response_with_pagination_links, | ||
adapter.serializable_hash(original_url: "http://example.com") | ||
adapter.serializable_hash(pagination: { original_url: "http://example.com" }) | ||
end | ||
|
||
def test_pagination_links_using_will_paginate | ||
serializer = ArraySerializer.new(using_will_paginate) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) | ||
|
||
assert_equal expected_response_with_pagination_links, | ||
adapter.serializable_hash(original_url: "http://example.com") | ||
adapter.serializable_hash(pagination: { original_url: "http://example.com" }) | ||
end | ||
|
||
def test_pagination_links_with_additional_params | ||
serializer = ArraySerializer.new(using_will_paginate) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: true) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) | ||
assert_equal expected_response_with_pagination_links_and_additional_params, | ||
adapter.serializable_hash(original_url: "http://example.com", | ||
query_parameters: { teste: "teste"}) | ||
adapter.serializable_hash(pagination: { original_url: "http://example.com", | ||
query_parameters: { teste: "teste"}}) | ||
|
||
end | ||
|
||
def test_not_showing_pagination_links | ||
serializer = ArraySerializer.new(using_will_paginate) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, pagination: false) | ||
serializer = ArraySerializer.new(@array) | ||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) | ||
|
||
assert_equal expected_response_without_pagination_links, adapter.serializable_hash | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍