Skip to content
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

Merged
merged 20 commits into from
Aug 22, 2015
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ test/version_tmp
tmp
*.swp
.ruby-version
.ruby-gemset
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
* adds cache support to attributes and associations [@joaomdmoura]
* uses model name to determine the type [@lsylvester]
* remove root key option and split JSON adapter [@joaomdmoura]
* adds FlattenJSON as default adapter [@joaomdmoura]
* adds FlattenJSON as default adapter [@joaomdmoura]
* adds support for `pagination links` at top level of JsonApi adapter [@bacarini]
Copy link
Member

Choose a reason for hiding this comment

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

👍

7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ If you wish to use a serializer other than the default, you can explicitly pass
render json: @posts, each_serializer: PostPreviewSerializer

# Or, you can explicitly provide the collection serializer as well
render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer
render json: @posts, serializer: CollectionSerializer, each_serializer: PostPreviewSerializer
```

### Meta
Expand Down Expand Up @@ -272,6 +272,11 @@ And you can change the JSON key that the serializer should use for a particular

The `url` declaration describes which named routes to use while generating URLs
for your JSON. Not every adapter will require URLs.
## Pagination

Pagination links will be included in your response automatically as long as the resource is paginated using [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate) and if you are using a ```JSON-API``` adapter.

Although the others adapters does not have this feature, it is possible to implement pagination links to `JSON` adapter. For more information about it, please see in our docs [How to add pagination links](https://github.com/rails-api/active_model_serializers/blob/master/docs/howto/add_pagination_links.md)

## Caching

Expand Down
2 changes: 2 additions & 0 deletions active_model_serializers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler", "~> 1.6"
spec.add_development_dependency "timecop", ">= 0.7"
spec.add_development_dependency "rake"
spec.add_development_dependency "kaminari"
spec.add_development_dependency "will_paginate"
end
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This is the documentation of AMS, it's focused on the **0.10.x version.**
## How to

- [How to add root key](howto/add_root_key.md)
- [How to add pagination links](howto/add_pagination_links.md)

## Getting Help

Expand Down
98 changes: 98 additions & 0 deletions docs/howto/add_pagination_links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# How to add pagination links

### JSON-API adapter

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.

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, PaginationLinks only will be included to JSON-API adapter, but you can still use a custom seriallizer to do so.

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

Copy link
Member

@bf4 bf4 Aug 18, 2015 via email

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is not!
I may do it, but it is another issue, no?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

The 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 😄

If you want pagination links in your response, use [Kaminari](https://github.com/amatsuda/kaminari) or [WillPaginate](https://github.com/mislav/will_paginate).

```ruby
#kaminari example
@posts = Kaminari.paginate_array(Post.all).page(3).per(1)
render json: @posts

#will_paginate example
@posts = Post.all.paginate(page: 3, per_page: 1)
render json: @posts
```
Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Member

Choose a reason for hiding this comment

The 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?

@posts = Post.page(3).per_page(1)

Or even

@array = Post.all; @posts = WillPaginate::Collection.create(page = 1, WillPaginate.per_page, @array.size) do |pager| pager.replace @array[pager.offset, pager.per_page].to_a end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey, @bf4 tks for the review.

I believe that, using WillPaginate::Collection we are overcomplicated it, since will_paginate already do it for us.
http://www.rubydoc.info/github/mislav/will_paginate/WillPaginate/Collection

Array.class_eval do
  def paginate(page = 1, per_page = 15)
    WillPaginate::Collection.create(page, per_page, size) do |pager|
      pager.replace self[pager.offset, pager.per_page].to_a
    end
  end
end

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

Copy link
Member

@bf4 bf4 Aug 20, 2015 via email

Choose a reason for hiding this comment

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


```ruby
ActiveModel::Serializer.config.adapter = :json_api
```

ex:
```json
{
"data": [
{
"type": "articles",
"id": "3",
"attributes": {
"title": "JSON API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2015-05-22T14:56:29.000Z",
"updated": "2015-05-22T14:56:28.000Z"
}
}
],
"links": {
"self": "http://example.com/articles?page[number]=3&page[size]=1",
"first": "http://example.com/articles?page[number]=1&page[size]=1",
"prev": "http://example.com/articles?page[number]=2&page[size]=1",
"next": "http://example.com/articles?page[number]=4&page[size]=1",
"last": "http://example.com/articles?page[number]=13&page[size]=1"
}
}
```

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.
Copy link
Member

Choose a reason for hiding this comment

The 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 current_page, total_pages, and size, such as are supported by both Kaminari or WillPaginate.

(links left out for ease of me typing :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍


### JSON adapter

If you are using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key.

In your action specify a custom serializer.
```ruby
render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer
```

And then, you could do something like the following class.
```ruby
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
```
ex.
```json
{
"articles": [
{
"id": 2,
"title": "JSON API paints my bikeshed!",
"body": "The shortest article. Ever."
}
],
"meta": {
"current_page": 3,
"next_page": 4,
"prev_page": 2,
"total_pages": 10,
"total_count": 10
}
}
```

### FlattenJSON adapter

This adapter does not allow us to use `meta` key, due to that it is not possible to add pagination links.
1 change: 1 addition & 0 deletions lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def use_adapter?

[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
define_method renderer_method do |resource, options|
options.fetch(:context) { options[:context] = request }
serializable_resource = get_serializer(resource, options)
super(serializable_resource, options)
end
Expand Down
20 changes: 20 additions & 0 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'active_model/serializer/adapter/json_api/fragment_cache'
require 'active_model/serializer/adapter/json_api/pagination_links'
Copy link
Member

Choose a reason for hiding this comment

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

👍


module ActiveModel
class Serializer
Expand Down Expand Up @@ -27,6 +28,8 @@ def serializable_hash(options = nil)
@hash[:included] |= result[:included]
end
end

add_links(options)
else
@hash[:data] = attributes_for_serializer(serializer, options)
add_resource_relationships(@hash[:data], serializer)
Expand Down Expand Up @@ -157,6 +160,23 @@ def add_resource_relationships(attrs, serializer, options = {})
end
end
end

def add_links(options)
links = @hash.fetch(:links) { {} }
resources = serializer.instance_variable_get(:@resource)
@hash[:links] = add_pagination_links(links, resources, options) if is_paginated?(resources)
end

def add_pagination_links(links, resources, options)
pagination_links = JsonApi::PaginationLinks.new(resources, options[:context]).serializable_hash(options)
links.update(pagination_links)
end

def is_paginated?(resource)
resource.respond_to?(:current_page) &&
resource.respond_to?(:total_pages) &&
resource.respond_to?(:size)
end
end
end
end
Expand Down
58 changes: 58 additions & 0 deletions lib/active_model/serializer/adapter/json_api/pagination_links.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module ActiveModel
class Serializer
class Adapter
class JsonApi < Adapter
class PaginationLinks
FIRST_PAGE = 1

attr_reader :collection, :context

def initialize(collection, context)
@collection = collection
@context = context
end

def serializable_hash(options = {})
pages_from.each_with_object({}) do |(key, value), hash|
params = query_parameters.merge(page: { number: value, size: collection.size }).to_query

hash[key] = "#{url(options)}?#{params}"
end
end

private

def pages_from
return {} if collection.total_pages == FIRST_PAGE

{}.tap do |pages|
pages[:self] = collection.current_page

unless collection.current_page == FIRST_PAGE
pages[:first] = FIRST_PAGE
pages[:prev] = collection.current_page - FIRST_PAGE
end

unless collection.current_page == collection.total_pages
pages[:next] = collection.current_page + FIRST_PAGE
pages[:last] = collection.total_pages
end
end
end

def url(options)
@url ||= options.fetch(:links, {}).fetch(:self, nil) || original_url
end

def original_url
@original_url ||= context.original_url[/\A[^?]+/]
end

def query_parameters
@query_parameters ||= context.query_parameters
end
end
end
end
end
end
Loading