api: maintain query parameters in pagination links

fixes #10491

test plan:
- make an api call to a paginated endpoint that has a query parameter as part
  of the call (courses/<id>/users with enrollment_type=student is a good one)
- the pagination link header links that come back should maintain the query
  parameter (in the example above, they would include enrollment_type=student)
- also try one that has an "include[]=" type parameter
- read the api pagination documentation (linked from the api sidebar) and make
  sure it makes sense.

Change-Id: I6c1649513553bb2ac9c1cfc137ff16c21e50a6a3
Reviewed-on: https://gerrit.instructure.com/13641
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Simon Williams 2012-09-11 15:12:36 -06:00
parent d511e04fee
commit 96ebee6dcc
14 changed files with 240 additions and 109 deletions

View File

@ -111,7 +111,7 @@ class ConversationsController < ApplicationController
# ]
def index
if request.format == :json
conversations = Api.paginate(@conversations_scope, self, request.request_uri.gsub(/(per_)?page=[^&]*(&|\z)/, '').sub(/[&?]\z/, ''))
conversations = Api.paginate(@conversations_scope, self, api_v1_conversations_url)
# optimize loading the most recent messages for each conversation into a single query
ConversationParticipant.preload_latest_messages(conversations, @current_user.id)
@conversations_json = conversations.map{ |c| conversation_json(c, @current_user, session, :include_participant_avatars => false, :include_participant_contexts => false, :visible => true) }

View File

@ -307,14 +307,16 @@ class CoursesController < ApplicationController
# If a user_id is passed in, modify the page parameter so that the page
# that contains that user is returned.
if params[:user_id] && user = users.scoped(:conditions => ["users.id = ?", params[:user_id]]).first
# We delete it from params so that it is not maintained in pagination links.
user_id = params.delete(:user_id)
if user_id.present? && user = users.scoped(:conditions => ["users.id = ?", user_id]).first
position_scope = users.scoped(:conditions => ["sortable_name <= ?", user.sortable_name])
position = position_scope.count(:select => "users.*", :distinct => true)
per_page = Api.per_page_for(self)
params[:page] = (position.to_f / per_page.to_f).ceil
end
users = Api.paginate(users, self, api_v1_course_users_path)
users = Api.paginate(users, self, api_v1_course_users_url)
includes = Array(params[:include])
user_json_preloads(users, includes.include?('email'))
if includes.include?('enrollments')

View File

@ -94,7 +94,7 @@ class SearchController < ApplicationController
@blank_fallback = !api_request?
max_results = [params[:per_page].try(:to_i) || 10, 50].min
max_results = Api.per_page_for(self)
if max_results < 1
if !types[:user] || params[:context]
max_results = nil # i.e. all results
@ -132,7 +132,7 @@ class SearchController < ApplicationController
def total_pages; nil; end
def per_page; #{max_results}; end
CODE
recipients = Api.paginate(recipients, self, request.request_uri.gsub(/(per_)?page=[^&]*(&|\z)/, '').sub(/[&?]\z/, ''))
recipients = Api.paginate(recipients, self, api_v1_search_recipients_url)
else
if contexts.size <= max_results / 2
recipients = contexts + participants

View File

@ -824,7 +824,7 @@ ActionController::Routing::Routes.draw do |map|
api.get 'conversations/find_recipients', :controller => :conversations, :action => :find_recipients
api.with_options(:controller => :conversations) do |conversations|
conversations.get 'conversations', :action => :index
conversations.get 'conversations', :action => :index, :path_name => 'conversations'
conversations.post 'conversations', :action => :create
conversations.post 'conversations/mark_all_as_read', :action => :mark_all_as_read
conversations.get 'conversations/:id', :action => :show

View File

@ -1,21 +1,31 @@
Pagination
==========
Requests that return multiple items will be paginated to 10 items by default. Further pages
can be requested with the `?page` query parameter. You can set a custom per-page amount
with the `?per_page` parameter. There is an unspecified limit to how big you can set
`per_page` to, so be sure to always check for the `Link` header.
Requests that return multiple items will be paginated to 10 items by default.
You can set a custom per-page amount with the `?per_page` parameter. There is
an unspecified limit to how big you can set `per_page` to, so be sure to always
check for the `Link` header.
To retrieve additional pages, the returned `Link` headers should be used. These
links should be treated as opaque. They will be absolute urls that include all
parameters necessary to retreive the desired next, previous, first, or last
page. The one exception is that if an access_token parameter is sent for
authentication, it will not be included in the returned links, and must be
re-appended.
Pagination information is provided in the [Link header](http://www.w3.org/Protocols/9707-link-header.html):
Link: </courses/:id/discussion_topics.json?page=2&per_page=10>; rel="next",
</courses/:id/discussion_topics.json?page=1&per_page=10>; rel="first",
</courses/:id/discussion_topics.json?page=5&per_page=10>; rel="last"
Link: <https://<canvas>/api/v1/courses/:id/discussion_topics.json?page=2&per_page=10>; rel="next",
<https://<canvas>/api/v1/courses/:id/discussion_topics.json?page=1&per_page=10>; rel="first",
<https://<canvas>/api/v1/courses/:id/discussion_topics.json?page=5&per_page=10>; rel="last"
The possible `rel` values are:
* next - link to the next page of results. None is sent if there is no next page.
* prev - link to the previous page of results. None is sent if there is no previous page.
* first - link to the first page of results. None is sent if there are no pages.
* last - link to the last page of results. None is sent if there are no pages, or if it
would be expensive to calculate the number of pages.
* next - link to the next page of results.
* prev - link to the previous page of results.
* first - link to the first page of results.
* last - link to the last page of results.
These will only be included if they are relevant. For example, the first page
of results will not contain a rel="prev" link. rel="last" may also be excluded
if the total cound is too expensive to compute on each request.

View File

@ -155,25 +155,38 @@ module Api
# a new, paginated collection will be returned
def self.paginate(collection, controller, base_url, pagination_args = {})
per_page = per_page_for(controller)
collection = collection.paginate({ :page => controller.params[:page], :per_page => per_page }.merge(pagination_args))
pagination_args.reverse_merge!({ :page => controller.params[:page], :per_page => per_page })
collection = collection.paginate(pagination_args)
return unless collection.respond_to?(:next_page)
links = []
base_url += (base_url =~ /\?/ ? '&': '?')
template = "<%spage=%s&per_page=#{collection.per_page}>; rel=\"%s\""
if collection.next_page
links << template % [base_url, collection.next_page, "next"]
end
if collection.previous_page
links << template % [base_url, collection.previous_page, "prev"]
end
links << template % [base_url, 1, "first"]
if !pagination_args[:without_count] && collection.total_pages && collection.total_pages > 1
links << template % [base_url, collection.total_pages, "last"]
end
total_pages = (pagination_args[:without_count] ? nil : collection.total_pages)
total_pages = nil if total_pages.to_i <= 1
links = build_links(base_url, {
:query_parameters => controller.request.query_parameters,
:per_page => collection.per_page,
:next => collection.next_page,
:prev => collection.previous_page,
:first => 1,
:last => total_pages,
})
controller.response.headers["Link"] = links.join(',') if links.length > 0
collection
end
EXCLUDE_IN_PAGINATION_LINKS = %w(page per_page access_token api_key)
def self.build_links(base_url, opts={})
links = []
base_url += (base_url =~ /\?/ ? '&': '?')
qp = opts[:query_parameters] || {}
qp = qp.with_indifferent_access.except(*EXCLUDE_IN_PAGINATION_LINKS)
base_url += "#{qp.to_query}&" if qp.present?
[:next, :prev, :first, :last].each do |k|
if opts[k].present?
links << "<#{base_url}page=#{opts[k]}&per_page=#{opts[:per_page]}>; rel=\"#{k}\""
end
end
links
end
def media_comment_json(media_object_or_hash)
media_object_or_hash = OpenStruct.new(media_object_or_hash) if media_object_or_hash.is_a?(Hash)
{

View File

@ -110,13 +110,23 @@ describe ConversationsController, :type => :integration do
{:controller => 'conversations', :action => 'index', :format => 'json', :scope => 'default', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/conversations.json?scope=default&page=2&per_page=3>; rel="next",</api/v1/conversations.json?scope=default&page=1&per_page=3>; rel="first",</api/v1/conversations.json?scope=default&page=3&per_page=3>; rel="last"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/conversations/ }.should be_true
links.all?{ |l| l.scan(/scope=default/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
# get the last page
json = api_call(:get, "/api/v1/conversations.json?scope=default&page=3&per_page=3",
{:controller => 'conversations', :action => 'index', :format => 'json', :scope => 'default', :page => '3', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/conversations.json?scope=default&page=2&per_page=3>; rel="prev",</api/v1/conversations.json?scope=default&page=1&per_page=3>; rel="first",</api/v1/conversations.json?scope=default&page=3&per_page=3>; rel="last"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/conversations/ }.should be_true
links.all?{ |l| l.scan(/scope=default/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
end
it "should filter conversations by scope" do

View File

@ -654,6 +654,18 @@ describe CoursesController, :type => :integration do
json.map { |u| u['enrollments'].map { |e| e['type'] } }.flatten.uniq.sort.should == %w{StudentEnrollment TeacherEnrollment}
end
it "maintains query parameters in link headers" do
json = api_call(
:get,
"/api/v1/courses/#{@course1.id}/users.json",
{ :controller => 'courses', :action => 'users', :course_id => @course1.id.to_s, :format => 'json' },
{ :enrollment_type => 'student', :maintain_params => '1', :per_page => 1 })
links = response['Link'].split(",")
links.should_not be_empty
links.all?{ |l| l =~ /enrollment_type=student/ }.should be_true
links.first.scan(/per_page/).length.should == 1
end
it "should not include sis user id or login id for non-admins" do
RoleOverride.create!(:context => Account.default, :permission => 'read_sis', :enrollment_type => 'TeacherEnrollment', :enabled => false)
student_in_course(:course => @course2, :active_all => true, :name => 'Zombo')

View File

@ -387,21 +387,21 @@ describe DiscussionTopicsController, :type => :integration do
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :course_id => @course.id.to_s, :per_page => '3'})
json.length.should == 3
response.headers['Link'].should == [
%{</api/v1/courses/#{@course.id}/discussion_topics?page=2&per_page=3>; rel="next"},
%{</api/v1/courses/#{@course.id}/discussion_topics?page=1&per_page=3>; rel="first"},
%{</api/v1/courses/#{@course.id}/discussion_topics?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/courses\/#{@course.id}\/discussion_topics/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
# get the last page
json = api_call(:get, "/api/v1/courses/#{@course.id}/discussion_topics.json?page=3&per_page=3",
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :course_id => @course.id.to_s, :page => '3', :per_page => '3'})
json.length.should == 1
response.headers['Link'].should == [
%{</api/v1/courses/#{@course.id}/discussion_topics?page=2&per_page=3>; rel="prev"},
%{</api/v1/courses/#{@course.id}/discussion_topics?page=1&per_page=3>; rel="first"},
%{</api/v1/courses/#{@course.id}/discussion_topics?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/courses\/#{@course.id}\/discussion_topics/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
end
it "should work with groups" do
@ -464,21 +464,21 @@ describe DiscussionTopicsController, :type => :integration do
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :group_id => group.id.to_s, :per_page => '3'})
json.length.should == 3
response.headers['Link'].should == [
%{</api/v1/groups/#{group.id}/discussion_topics?page=2&per_page=3>; rel="next"},
%{</api/v1/groups/#{group.id}/discussion_topics?page=1&per_page=3>; rel="first"},
%{</api/v1/groups/#{group.id}/discussion_topics?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/groups\/#{group.id}\/discussion_topics/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
# get the last page
json = api_call(:get, "/api/v1/groups/#{group.id}/discussion_topics.json?page=3&per_page=3",
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :group_id => group.id.to_s, :page => '3', :per_page => '3'})
json.length.should == 1
response.headers['Link'].should == [
%{</api/v1/groups/#{group.id}/discussion_topics?page=2&per_page=3>; rel="prev"},
%{</api/v1/groups/#{group.id}/discussion_topics?page=1&per_page=3>; rel="first"},
%{</api/v1/groups/#{group.id}/discussion_topics?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/groups\/#{group.id}\/discussion_topics/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
end
it "should fulfill module viewed requirements when marking a topic read" do
@ -720,11 +720,11 @@ describe DiscussionTopicsController, :type => :integration do
:course_id => @course.id.to_s, :topic_id => @topic.id.to_s, :per_page => '3' })
json.length.should == 3
json.map{ |e| e['id'] }.should == entries.last(3).reverse.map{ |e| e.id }
response.headers['Link'].should == [
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries?page=2&per_page=3>; rel="next"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries?page=1&per_page=3>; rel="first"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/courses\/#{@course.id}\/discussion_topics\/#{@topic.id}\/entries/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
# last page
json = api_call(
@ -733,11 +733,11 @@ describe DiscussionTopicsController, :type => :integration do
:course_id => @course.id.to_s, :topic_id => @topic.id.to_s, :page => '3', :per_page => '3' })
json.length.should == 2
json.map{ |e| e['id'] }.should == [entries.first, @entry].map{ |e| e.id }
response.headers['Link'].should == [
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries?page=2&per_page=3>; rel="prev"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries?page=1&per_page=3>; rel="first"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/courses\/#{@course.id}\/discussion_topics\/#{@topic.id}\/entries/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
end
it "should only include the first 10 replies for each top-level entry" do
@ -810,11 +810,11 @@ describe DiscussionTopicsController, :type => :integration do
:course_id => @course.id.to_s, :topic_id => @topic.id.to_s, :entry_id => @entry.id.to_s, :per_page => '3' })
json.length.should == 3
json.map{ |e| e['id'] }.should == replies.last(3).reverse.map{ |e| e.id }
response.headers['Link'].should == [
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries/#{@entry.id}/replies?page=2&per_page=3>; rel="next"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries/#{@entry.id}/replies?page=1&per_page=3>; rel="first"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries/#{@entry.id}/replies?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/courses\/#{@course.id}\/discussion_topics\/#{@topic.id}\/entries\/#{@entry.id}\/replies/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
# last page
json = api_call(
@ -823,11 +823,11 @@ describe DiscussionTopicsController, :type => :integration do
:course_id => @course.id.to_s, :topic_id => @topic.id.to_s, :entry_id => @entry.id.to_s, :page => '3', :per_page => '3' })
json.length.should == 2
json.map{ |e| e['id'] }.should == [replies.first, @reply].map{ |e| e.id }
response.headers['Link'].should == [
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries/#{@entry.id}/replies?page=2&per_page=3>; rel="prev"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries/#{@entry.id}/replies?page=1&per_page=3>; rel="first"},
%{</api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/entries/#{@entry.id}/replies?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/courses\/#{@course.id}\/discussion_topics\/#{@topic.id}\/entries\/#{@entry.id}\/replies/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
end
end

View File

@ -199,21 +199,21 @@ describe ExternalToolsController, :type => :integration do
{:controller => 'external_tools', :action => 'index', :format => 'json', :"#{type}_id" => context.id.to_s, :per_page => '3'})
json.length.should == 3
response.headers['Link'].should == [
%{</api/v1/#{type}s/#{context.id}/external_tools?page=2&per_page=3>; rel="next"},
%{</api/v1/#{type}s/#{context.id}/external_tools?page=1&per_page=3>; rel="first"},
%{</api/v1/#{type}s/#{context.id}/external_tools?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/#{type}s\/#{context.id}\/external_tools/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3/
# get the last page
json = api_call(:get, "/api/v1/#{type}s/#{context.id}/external_tools.json?page=3&per_page=3",
{:controller => 'external_tools', :action => 'index', :format => 'json', :"#{type}_id" => context.id.to_s, :per_page => '3', :page => '3'})
json.length.should == 1
response.headers['Link'].should == [
%{</api/v1/#{type}s/#{context.id}/external_tools?page=2&per_page=3>; rel="prev"},
%{</api/v1/#{type}s/#{context.id}/external_tools?page=1&per_page=3>; rel="first"},
%{</api/v1/#{type}s/#{context.id}/external_tools?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/#{type}s\/#{context.id}\/external_tools/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3/
end
def tool_with_everything(context, opts={})

View File

@ -169,19 +169,19 @@ describe "Files API", :type => :integration do
7.times {|i| Attachment.create!(:filename => "test#{i}.txt", :display_name => "test#{i}.txt", :uploaded_data => StringIO.new('file'), :folder => @root, :context => @course) }
json = api_call(:get, "/api/v1/folders/#{@root.id}/files?per_page=3", @files_path_options.merge(:id => @root.id.to_param, :per_page => '3'), {})
json.length.should == 3
response.headers['Link'].should == [
%{<http://www.example.com/api/v1/folders/#{@root.id}/files?page=2&per_page=3>; rel="next"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/files?page=1&per_page=3>; rel="first"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/files?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/folders\/#{@root.id}\/files/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3/
json = api_call(:get, "/api/v1/folders/#{@root.id}/files?per_page=3&page=3", @files_path_options.merge(:id => @root.id.to_param, :per_page => '3', :page => '3'), {})
json.length.should == 1
response.headers['Link'].should == [
%{<http://www.example.com/api/v1/folders/#{@root.id}/files?page=2&per_page=3>; rel="prev"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/files?page=1&per_page=3>; rel="first"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/files?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/folders\/#{@root.id}\/files/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3/
end
end

View File

@ -84,19 +84,19 @@ describe "Folders API", :type => :integration do
7.times {|i| @root.sub_folders.create!(:name => "folder#{i}", :context => @course) }
json = api_call(:get, @folders_path + "/#{@root.id}/folders?per_page=3", @folders_path_options.merge(:per_page => '3'), {})
json.length.should == 3
response.headers['Link'].should == [
%{<http://www.example.com/api/v1/folders/#{@root.id}/folders?page=2&per_page=3>; rel="next"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/folders?page=1&per_page=3>; rel="first"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/folders?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/folders\/#{@root.id}\/folders/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
json = api_call(:get, @folders_path + "/#{@root.id}/folders?per_page=3&page=3", @folders_path_options.merge(:per_page => '3', :page => '3'), {})
json.length.should == 1
response.headers['Link'].should == [
%{<http://www.example.com/api/v1/folders/#{@root.id}/folders?page=2&per_page=3>; rel="prev"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/folders?page=1&per_page=3>; rel="first"},
%{<http://www.example.com/api/v1/folders/#{@root.id}/folders?page=3&per_page=3>; rel="last"}
].join(',')
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/folders\/#{@root.id}\/folders/ }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=3&per_page=3>/
end
end

View File

@ -267,13 +267,23 @@ describe SearchController, :type => :integration do
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :type => 'user', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=cletus&type=user&page=2&per_page=3>; rel="next",</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="first"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/search\/recipients/ }.should be_true
links.all?{ |l| l.scan(/search=cletus/).size == 1 }.should be_true
links.all?{ |l| l.scan(/type=user/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
# get the next page
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&page=2&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :type => 'user', :page => '2', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="prev",</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=3>; rel="first"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/search\/recipients/ }.should be_true
links.all?{ |l| l.scan(/search=cletus/).size == 1 }.should be_true
links.all?{ |l| l.scan(/type=user/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
end
it "should allow fetching all users iff a context is specified" do
@ -284,7 +294,12 @@ describe SearchController, :type => :integration do
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&per_page=-1",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :type => 'user', :per_page => '-1'})
json.size.should eql 10
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=cletus&type=user&page=2&per_page=10>; rel="next",</api/v1/search/recipients.json?search=cletus&type=user&page=1&per_page=10>; rel="first"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/search\/recipients/ }.should be_true
links.all?{ |l| l.scan(/search=cletus/).size == 1 }.should be_true
links.all?{ |l| l.scan(/type=user/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=10>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=10>/
json = api_call(:get, "/api/v1/search/recipients.json?search=cletus&type=user&context=course_#{@course.id}&per_page=-1",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'cletus', :context => "course_#{@course.id}", :type => 'user', :per_page => '-1'})
@ -301,13 +316,23 @@ describe SearchController, :type => :integration do
json = api_call(:get, "/api/v1/search/recipients.json?search=ofcourse&type=context&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :per_page => '3'})
json.size.should eql 3
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=ofcourse&type=context&page=2&per_page=3>; rel="next",</api/v1/search/recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="first"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/search\/recipients/ }.should be_true
links.all?{ |l| l.scan(/search=ofcourse/).size == 1 }.should be_true
links.all?{ |l| l.scan(/type=context/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=2&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
# get the next page
json = api_call(:get, "/api/v1/search/recipients.json?search=ofcourse&type=context&page=2&per_page=3",
{:controller => 'search', :action => 'recipients', :format => 'json', :search => 'ofcourse', :type => 'context', :page => '2', :per_page => '3'})
json.size.should eql 1
response.headers['Link'].should eql(%{</api/v1/search/recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="prev",</api/v1/search/recipients.json?search=ofcourse&type=context&page=1&per_page=3>; rel="first"})
links = response.headers['Link'].split(",")
links.all?{ |l| l =~ /api\/v1\/search\/recipients/ }.should be_true
links.all?{ |l| l.scan(/search=ofcourse/).size == 1 }.should be_true
links.all?{ |l| l.scan(/type=context/).size == 1 }.should be_true
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=1&per_page=3>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=3>/
end
it "should allow fetching all contexts" do

View File

@ -529,4 +529,63 @@ describe Api do
res.should == html
end
end
context ".build_links" do
it "should not build links if not pagination is provided" do
Api.build_links("www.example.com").should be_empty
end
it "should not build links for empty pages" do
Api.build_links("www.example.com/", {
:per_page => 10,
:next => "",
:prev => "",
:first => "",
:last => "",
}).should be_empty
end
it "should build next, prev, first, and last links if provided" do
links = Api.build_links("www.example.com/", {
:per_page => 10,
:next => 4,
:prev => 2,
:first => 1,
:last => 10,
})
links.all?{ |l| l =~ /www.example.com\/\?/ }.should be_true
links.find{ |l| l.match(/rel="next"/)}.should =~ /page=4&per_page=10>/
links.find{ |l| l.match(/rel="prev"/)}.should =~ /page=2&per_page=10>/
links.find{ |l| l.match(/rel="first"/)}.should =~ /page=1&per_page=10>/
links.find{ |l| l.match(/rel="last"/)}.should =~ /page=10&per_page=10>/
end
it "should maintain query parameters" do
links = Api.build_links("www.example.com/", {
:query_parameters => { :search => "hihi" },
:per_page => 10,
:next => 2,
})
links.first.should == "<www.example.com/?search=hihi&page=2&per_page=10>; rel=\"next\""
end
it "should maintain array query parameters" do
links = Api.build_links("www.example.com/", {
:query_parameters => { :include => ["enrollments"] },
:per_page => 10,
:next => 2,
})
qs = "#{CGI.escape("include[]")}=enrollments"
links.first.should == "<www.example.com/?#{qs}&page=2&per_page=10>; rel=\"next\""
end
it "should not include certain sensitive params in the link headers" do
links = Api.build_links("www.example.com/", {
:query_parameters => { :access_token => "blah", :api_key => "xxx", :page => 3, :per_page => 10 },
:per_page => 10,
:next => 4,
})
links.first.should == "<www.example.com/?page=4&per_page=10>; rel=\"next\""
end
end
end