page revisions api

test plan:
 1. consult the Pages API documentation, and notice
   (a) the new "List revisions" endpoint
       and the PageRevision data type it returns
   (b) the "Show revision" endpoint
       (which can accept an id or "latest")
   (c) the "Revert to revision" endpoint
 2. exercise these endpoints as a teacher
   (a) note that reverting a page should not change
       editing roles, hidden, or published state
 3. exercise these endpoints as a student
   (a) students should not be able to list,
       or retrieve prior revisions unless they have
       edit rights to the page
   (b) all students (who can read the page) have permission
       to get the latest version (but edit rights
       are required to get a specific revision number)
   (c) for the revert action, the course permissions must
       allow students to edit wiki pages; if the course
       does not grant wiki edit rights to the student,
       but the page does, the student can change page content
       only (cannot revert or rename)
   (d) for the show revision action, the permissions of the current
       version of the page are applied; students with edit rights
       to it can view or revert to previous versions that
       may have been hidden or unpublished (in other words,
       "hidden" and "unpublished" apply only to the current
       state of the page, not historical versions of it)

fixes CNVS-7301

Change-Id: Ie4948617e24154a61f3111e08fc3f89b9265da6d
Reviewed-on: https://gerrit.instructure.com/23088
Reviewed-by: Mark Severson <markse@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Hannah Bottalla <hannah@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Bracken Mosbacker <bracken@instructure.com>
This commit is contained in:
Jeremy Stanley 2013-08-06 13:43:48 -06:00
parent e04b052d87
commit b474205c2a
5 changed files with 449 additions and 15 deletions

View File

@ -19,7 +19,7 @@
# @API Pages
#
# Pages are rich content associated with Courses and Groups in Canvas.
# The Pages API allows you to create, retrieve, update, and delete ages.
# The Pages API allows you to create, retrieve, update, and delete pages.
#
# @object Page
# {
@ -76,7 +76,37 @@
# },
#
# // (Optional) An explanation of why this is locked for the user. Present when locked_for_user is true.
# lock_explanation: "This discussion is locked until September 1 at 12:00am"
# lock_explanation: "This page is locked until September 1 at 12:00am"
# }
#
# @object PageRevision
# {
# // an identifier for this revision of the page
# revision_id: 7,
#
# // the time when this revision was saved
# updated_at: '2012-08-07T11:23:58-06:00',
#
# // the User who saved this revision, if applicable
# // (this may not be present if the page was imported from another system)
# edited_by: {
# id: 1123,
# display_name: "Leonardo Fibonacci",
# avatar_image_url: "https://canvas.example.com/images/thumbnails/bWVhbmluZ2xlc3M=",
# html_url: "https://canvas.example.com/courses/789/users/1123"
# }
#
# // the following fields are not included in the index action
# // and may be omitted from the show action via summary=1
#
# // the historic url of the page
# url: "old-page-title",
#
# // the historic page title
# title: "Old Page Title",
#
# // the historic page contents
# body: "<p>Old Page Content</p>"
# }
class WikiPagesApiController < ApplicationController
before_filter :require_context
@ -138,7 +168,7 @@ class WikiPagesApiController < ApplicationController
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/front_page
# https://<canvas>/api/v1/groups/456/front_page
#
# @returns Page
def show
@ -242,7 +272,90 @@ class WikiPagesApiController < ApplicationController
end
end
end
# @API List revisions
#
# List the revisions of a page. Callers must have update rights on the page in order to see page history.
#
# @argument url the unique identifier for a page
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions
#
# @returns [PageRevision]
def revisions
if authorized_action(@page, @current_user, :read_revisions)
route = polymorphic_url([:api_v1, @context, @page, :revisions])
scope = @page.versions
revisions = Api.paginate(scope, self, route)
render :json => wiki_page_revisions_json(revisions, @current_user, session)
end
end
# @API Show revision
#
# Retrieve the metadata and optionally content of a revision of the page.
# Note that retrieving historic versions of pages requires edit rights.
#
# @argument url the unique identifier for a page
# @argument summary [optional] [boolean] if set, exclude page content from results
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions/latest
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions/4
#
# @returns PageRevision
def show_revision
if params.has_key?(:revision_id)
permission = :read_revisions
revision = @page.versions.find_by_number!(params[:revision_id].to_i)
else
permission = :read
revision = @page.versions.current
end
if authorized_action(@page, @current_user, permission)
include_content = if params.has_key?(:summary)
!value_to_boolean(params[:summary])
else
true
end
render :json => wiki_page_revision_json(revision, @current_user, session, include_content)
end
end
# @API Revert to revision
#
# Revert a page to a prior revision.
#
# @argument url the unique identifier for the page
# @argument revision_id the revision to revert to (use the {api:WikiPagesApiController#revisions List Revisions API} to see available revisions)
#
# @example_request
# curl -X POST -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/123/pages/the-page-url/revisions/6
#
# @returns PageRevision
def revert
if authorized_action(@page, @current_user, :read_revisions) && authorized_action(@page, @current_user, :update)
revision_id = params[:revision_id].to_i
@revision = @page.versions.find_by_number!(revision_id).model
@page.body = @revision.body
@page.title = @revision.title
@page.url = @revision.url
@page.user_id = @current_user.id if @current_user
if @page.save
render :json => wiki_page_revision_json(@page.versions.current, @current_user, session, true)
else
render :json => @page.errors.to_json, :status => :bad_request
end
end
end
protected
def get_wiki_page

View File

@ -234,16 +234,16 @@ class WikiPage < ActiveRecord::Base
can :read
given {|user, session| user && self.can_edit_page?(user)}
can :update_content
can :update_content and can :read_revisions
given {|user, session| user && self.can_edit_page?(user) && self.wiki.grants_right?(user, session, :create_page)}
can :create
given {|user, session| user && self.can_edit_page?(user) && self.wiki.grants_right?(user, session, :update_page)}
can :update
can :update and can :read_revisions
given {|user, session| user && self.can_edit_page?(user) && self.active? && self.wiki.grants_right?(user, session, :update_page_content)}
can :update_content
can :update_content and can :read_revisions
given {|user, session| user && self.can_edit_page?(user) && self.active? && self.wiki.grants_right?(user, session, :delete_page)}
can :delete

View File

@ -1233,6 +1233,14 @@ FakeRails3Routes.draw do
get "groups/:group_id/pages/:url", :action => :show, :path_name => 'group_wiki_page'
get "courses/:course_id/front_page", :action => :show
get "groups/:group_id/front_page", :action => :show
get "courses/:course_id/pages/:url/revisions", :action => :revisions, :path_name => 'course_wiki_page_revisions'
get "groups/:group_id/pages/:url/revisions", :action => :revisions, :path_name => 'group_wiki_page_revisions'
get "courses/:course_id/pages/:url/revisions/latest", :action => :show_revision
get "groups/:group_id/pages/:url/revisions/latest", :action => :show_revision
get "courses/:course_id/pages/:url/revisions/:revision_id", :action => :show_revision
get "groups/:group_id/pages/:url/revisions/:revision_id", :action => :show_revision
post "courses/:course_id/pages/:url/revisions/:revision_id", :action => :revert
post "groups/:group_id/pages/:url/revisions/:revision_id", :action => :revert
post "courses/:course_id/pages", :action => :create
post "groups/:group_id/pages", :action => :create
put "courses/:course_id/pages/:url", :action => :update

View File

@ -42,4 +42,25 @@ module Api::V1::WikiPage
def wiki_pages_json(wiki_pages, current_user, session)
wiki_pages.map { |page| wiki_page_json(page, current_user, session, false) }
end
def wiki_page_revision_json(version, current_user, current_session, include_content = true)
page = version.model
hash = {
'revision_id' => version.number,
'updated_at' => page.updated_at
}
if include_content
hash.merge!({
'url' => page.url,
'title' => page.title,
'body' => api_user_content(page.body)
})
end
hash['edited_by'] = user_display_json(page.user, page.context) if page.user
hash
end
def wiki_page_revisions_json(versions, current_user, current_session)
versions.map { |ver| wiki_page_revision_json(ver, current_user, current_session, false) }
end
end

View File

@ -19,6 +19,14 @@ require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../locked_spec')
describe "Pages API", :type => :integration do
include Api::V1::User
def avatar_url_for_user(user, *a)
"http://www.example.com/images/messages/avatar-50.png"
end
def blank_fallback
nil
end
context 'locked api item' do
let(:item_type) { 'page' }
@ -144,14 +152,6 @@ describe "Pages API", :type => :integration do
end
describe "show" do
include Api::V1::User
def avatar_url_for_user(user, *a)
"http://www.example.com/images/messages/avatar-50.png"
end
def blank_fallback
nil
end
before do
@teacher.short_name = 'the teacher'
@teacher.save!
@ -211,6 +211,142 @@ describe "Pages API", :type => :integration do
end
end
describe "revisions" do
before do
@timestamps = %w(2013-01-01 2013-01-02 2013-01-03).map { |d| Time.zone.parse(d) }
course_with_ta :course => @course, :active_all => true
Timecop.freeze(@timestamps[0]) do # rev 1
@vpage = @course.wiki.wiki_pages.build :title => 'version test page'
@vpage.workflow_state = 'unpublished'
@vpage.body = 'draft'
@vpage.save!
end
Timecop.freeze(@timestamps[1]) do # rev 2
@vpage.workflow_state = 'active'
@vpage.body = 'published by teacher'
@vpage.user = @teacher
@vpage.save!
end
Timecop.freeze(@timestamps[2]) do # rev 3
@vpage.body = 'revised by ta'
@vpage.user = @ta
@vpage.save!
end
@user = @teacher
end
it "should list revisions of a page" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions",
:controller=>"wiki_pages_api", :action=>"revisions", :format=>"json",
:course_id=>@course.to_param, :url=>@vpage.url)
json.should == [
{
'revision_id' => 3,
'updated_at' => @timestamps[2].as_json,
'edited_by' => user_display_json(@ta, @course).stringify_keys!,
},
{
'revision_id' => 2,
'updated_at' => @timestamps[1].as_json,
'edited_by' => user_display_json(@teacher, @course).stringify_keys!,
},
{
'revision_id' => 1,
'updated_at' => @timestamps[0].as_json,
}
]
end
it "should summarize the latest revision" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/latest?summary=true",
:controller => "wiki_pages_api", :action => "show_revision", :format => "json",
:course_id => @course.to_param, :url => @vpage.url, :summary => 'true')
json.should == {
'revision_id' => 3,
'updated_at' => @timestamps[2].as_json,
'edited_by' => user_display_json(@ta, @course).stringify_keys!,
}
end
it "should paginate the revision list" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions?per_page=2",
:controller=>"wiki_pages_api", :action=>"revisions", :format=>"json",
:course_id=>@course.to_param, :url=>@vpage.url, :per_page=>'2')
json.size.should == 2
json += api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions?per_page=2&page=2",
:controller=>"wiki_pages_api", :action=>"revisions", :format=>"json",
:course_id=>@course.to_param, :url=>@vpage.url, :per_page=>'2', :page=>'2')
json.map { |r| r['revision_id'] }.should == [3, 2, 1]
end
it "should retrieve an old revision" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/1",
:controller=>"wiki_pages_api", :action=>"show_revision", :format=>"json", :course_id=>"#{@course.id}", :url=>@vpage.url, :revision_id=>'1')
json.should == {
'body' => 'draft',
'title' => 'version test page',
'url' => @vpage.url,
'updated_at' => @timestamps[0].as_json,
'revision_id' => 1
}
end
it "should retrieve the latest revision" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/latest",
:controller=>"wiki_pages_api", :action=>"show_revision", :format=>"json", :course_id=>"#{@course.id}", :url=>@vpage.url)
json.should == {
'body' => 'revised by ta',
'title' => 'version test page',
'url' => @vpage.url,
'updated_at' => @timestamps[2].as_json,
'revision_id' => 3,
'edited_by' => user_display_json(@ta, @course).stringify_keys!
}
end
it "should revert to a prior revision" do
json = api_call(:post, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/2",
:controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :course_id=>@course.to_param,
:url=>@vpage.url, :revision_id=>'2')
json['body'].should == 'published by teacher'
json['revision_id'].should == 4
@vpage.reload.body.should == 'published by teacher'
end
it "should revert page content only" do
@vpage.hide_from_students = true
@vpage.workflow_state = 'unpublished'
@vpage.title = 'booga!'
@vpage.body = 'booga booga!'
@vpage.editing_roles = 'teachers,students,public'
@vpage.save! # rev 4
api_call(:post, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/3",
:controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :course_id=>@course.to_param,
:url=>@vpage.url, :revision_id=>'3')
@vpage.reload
@vpage.hide_from_students.should be_true
@vpage.should be_unpublished
@vpage.editing_roles.should == 'teachers,students,public'
@vpage.title.should == 'version test page' # <- reverted
@vpage.body.should == 'revised by ta' # <- reverted
@vpage.user_id.should == @teacher.id # the user who performed the revert (not the original author)
end
it "show should 404 when given a bad revision number" do
api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/99",
{ :controller=>"wiki_pages_api", :action=>"show_revision", :format=>"json", :course_id=>"#{@course.id}",
:url=>@vpage.url, :revision_id=>'99' }, {}, {}, { :expected_status => 404 })
end
it "revert should 404 when given a bad revision number" do
api_call(:post, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/99",
{ :controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :course_id=>"#{@course.id}",
:url=>@vpage.url, :revision_id=>'99' }, {}, {}, { :expected_status => 404 })
end
end
describe "create" do
it "should require a title" do
api_call(:post, "/api/v1/courses/#{@course.id}/pages",
@ -710,6 +846,117 @@ describe "Pages API", :type => :integration do
{}, {}, {:expected_status => 401})
end
end
context "revisions" do
before do
@vpage = @course.wiki.wiki_pages.build :title => 'student version test page', :body => 'draft'
@vpage.workflow_state = 'unpublished'
@vpage.save! # rev 1
@vpage.hide_from_students = true
@vpage.workflow_state = 'active'
@vpage.body = 'published but hidden'
@vpage.save! # rev 2
@vpage.hide_from_students = false
@vpage.body = 'now visible to students'
@vpage.save! # rev 3
end
it "should refuse to list revisions" do
api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions",
{ :controller => "wiki_pages_api", :action => "revisions", :format => "json",
:course_id => @course.to_param, :url => @vpage.url }, {}, {},
{ :expected_status => 401 })
end
it "should refuse to retrieve a revision" do
api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/3",
{ :controller => "wiki_pages_api", :action => "show_revision", :format => "json", :course_id => "#{@course.id}",
:url => @vpage.url, :revision_id => '3' }, {}, {}, { :expected_status => 401 })
end
it "should refuse to revert a page" do
api_call(:post, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/2",
{ :controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :course_id=>@course.to_param,
:url=>@vpage.url, :revision_id=>'2' }, {}, {}, { :expected_status => 401 })
end
it "should describe the latest version" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/latest",
:controller => "wiki_pages_api", :action => "show_revision", :format => "json",
:course_id => @course.to_param, :url => @vpage.url)
json['revision_id'].should == 3
end
context "with page-level student editing role" do
before do
@vpage.editing_roles = 'teachers,students'
@vpage.body = 'with student editing roles'
@vpage.save! # rev 4
end
it "should list revisions" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions",
:controller => "wiki_pages_api", :action => "revisions", :format => "json",
:course_id => @course.to_param, :url => @vpage.url)
json.map { |r| r['revision_id'] }.should == [4, 3, 2, 1]
end
it "should retrieve an old revision" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/3",
:controller => "wiki_pages_api", :action => "show_revision", :format => "json", :course_id => "#{@course.id}",
:url => @vpage.url, :revision_id => '3')
json['body'].should == 'now visible to students'
end
it "should retrieve a (formerly) hidden revision" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/2",
:controller => "wiki_pages_api", :action => "show_revision", :format => "json", :course_id => "#{@course.id}",
:url => @vpage.url, :revision_id => '2')
json['body'].should == 'published but hidden'
end
it "should retrieve a (formerly) unpublished revision" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/1",
:controller => "wiki_pages_api", :action => "show_revision", :format => "json", :course_id => "#{@course.id}",
:url => @vpage.url, :revision_id => '1')
json['body'].should == 'draft'
end
it "should not retrieve a version of a locked page" do
mod = @course.context_modules.create! :name => 'bad module'
mod.add_item(:id => @vpage.id, :type => 'wiki_page')
mod.unlock_at = 1.year.from_now
mod.save!
json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/3",
{ :controller => "wiki_pages_api", :action => "show_revision", :format => "json", :course_id => "#{@course.id}",
:url => @vpage.url, :revision_id => '3' }, {}, {}, { :expected_status => 401 })
end
it "should not revert page content" do
api_call(:post, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/2",
{ :controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :course_id=>@course.to_param,
:url=>@vpage.url, :revision_id=>'2' }, {}, {}, { :expected_status => 401 })
end
end
context "with course-level student editing role" do
before do
@course.default_wiki_editing_roles = 'teachers,students'
@course.save!
end
it "should revert page content" do
api_call(:post, "/api/v1/courses/#{@course.id}/pages/#{@vpage.url}/revisions/2",
:controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :course_id=>@course.to_param,
:url=>@vpage.url, :revision_id=>'2')
@vpage.reload
@vpage.hide_from_students.should be_false # permissions aren't (conceptually) versioned
@vpage.body.should == 'published but hidden'
end
end
end
end
context "group" do
@ -752,5 +999,50 @@ describe "Pages API", :type => :integration do
{:controller=>'wiki_pages_api', :action=>'destroy', :format=>'json', :group_id=>@group.to_param, :url=>testpage.url})
@group.reload.wiki.wiki_pages.not_deleted.size.should == count - 1
end
context "revisions" do
before do
@vpage = @group.wiki.wiki_pages.create! :title => 'revision test page', :body => 'old version'
@vpage.body = 'new version'
@vpage.save!
end
it "should list revisions for a page" do
json = api_call(:get, "/api/v1/groups/#{@group.id}/pages/#{@vpage.url}/revisions",
:controller => 'wiki_pages_api', :action => 'revisions', :format => 'json',
:group_id => @group.to_param, :url => @vpage.url)
json.map { |v| v['revision_id'] }.should == [2, 1]
end
it "should retrieve an old revision of a page" do
json = api_call(:get, "/api/v1/groups/#{@group.id}/pages/#{@vpage.url}/revisions/1",
:controller => 'wiki_pages_api', :action => 'show_revision', :format => 'json',
:group_id => @group.to_param, :url => @vpage.url, :revision_id => '1')
json['body'].should == 'old version'
end
it "should retrieve the latest version of a page" do
json = api_call(:get, "/api/v1/groups/#{@group.id}/pages/#{@vpage.url}/revisions/latest",
:controller => 'wiki_pages_api', :action => 'show_revision', :format => 'json',
:group_id => @group.to_param, :url => @vpage.url)
json['body'].should == 'new version'
end
it "should revert to an old version of a page" do
api_call(:post, "/api/v1/groups/#{@group.id}/pages/#{@vpage.url}/revisions/1",
{ :controller=>"wiki_pages_api", :action=>"revert", :format=>"json", :group_id=>@group.to_param,
:url=>@vpage.url, :revision_id=>'1' })
@vpage.reload.body.should == 'old version'
end
it "should summarize the latest version" do
json = api_call(:get, "/api/v1/groups/#{@group.id}/pages/#{@vpage.url}/revisions/latest?summary=1",
:controller => "wiki_pages_api", :action => "show_revision", :format => "json",
:group_id => @group.to_param, :url => @vpage.url, :summary => '1')
json['revision_id'].should == 2
json['body'].should be_nil
end
end
end
end