From 8bdfdd3103727f40b2cdfabf3c9208d896d25b44 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Fri, 19 Apr 2013 13:55:00 -0600 Subject: [PATCH] wiki pages CRUD API fixes CNVS-5279 test plan: - see API documentation to create/retrieve/update/delete pages - exercise endpoints, including - CRUD functionality (above) - test renaming page - test publishing / unpublishing - test creating the front page in a new course or group Change-Id: I5d3b36615b7bdbfda0d4781db14cd7d49d32eba1 Reviewed-on: https://gerrit.instructure.com/19952 Tested-by: Jenkins Reviewed-by: Bracken Mosbacker Product-Review: Bracken Mosbacker QA-Review: Adam Phillipps --- app/controllers/wiki_pages_api_controller.rb | 236 ++++++++++++ app/controllers/wiki_pages_controller.rb | 92 +---- app/models/wiki_page.rb | 16 +- config/routes.rb | 16 +- lib/api/v1/wiki_page.rb | 11 +- spec/apis/user_content_spec.rb | 6 +- spec/apis/v1/pages_api_spec.rb | 380 ++++++++++++++++--- 7 files changed, 593 insertions(+), 164 deletions(-) create mode 100644 app/controllers/wiki_pages_api_controller.rb diff --git a/app/controllers/wiki_pages_api_controller.rb b/app/controllers/wiki_pages_api_controller.rb new file mode 100644 index 00000000000..ec895a7c1ca --- /dev/null +++ b/app/controllers/wiki_pages_api_controller.rb @@ -0,0 +1,236 @@ +# +# Copyright (C) 2011 Instructure, Inc. +# +# This file is part of Canvas. +# +# Canvas is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, version 3 of the License. +# +# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# + +# @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. +# +# @object Page +# { +# // the unique locator for the page +# url: "my-page-title", +# +# // the title of the page +# title: "My Page Title", +# +# // the creation date for the page +# created_at: "2012-08-06T16:46:33-06:00", +# +# // the date the page was last updated +# updated_at: "2012-08-08T14:25:20-06:00", +# +# // whether this page is hidden from students +# // (note: students will never see this true; pages hidden from them will be omitted from results) +# hide_from_students: false, +# +# // roles allowed to edit the page; comma-separated list comprising a combination of +# // 'teachers', 'students', and/or 'public' +# // if not supplied, course defaults are used +# editing_roles: "teachers,students", +# +# // the User who last edited the page +# // (this may not be present if the page was imported from another system) +# last_edited_by: { +# id: 133, +# display_name: "Rey del Pueblo", +# avatar_image_url: "https://canvas.example.com/images/thumbnails/bm90aGluZyBoZXJl", +# html_url: "https://canvas.example.com/courses/789/users/133" +# }, +# +# // the page content, in HTML +# // (present when requesting a single page; omitted when listing pages) +# body: "

Page Content

", +# +# // whether the page is published +# published: true +# } +class WikiPagesApiController < ApplicationController + before_filter :require_context + before_filter :get_wiki_page, :except => [:create, :index] + + include Api::V1::WikiPage + + # @API List pages + # + # List the wiki pages associated with a course or group + # + # @argument sort [optional] Sort results by this field: one of 'title', 'created_at', or 'updated_at' + # @argument order [optional] The sorting order: 'asc' (default) or 'desc' + # + # @example_request + # curl -H 'Authorization: Bearer ' \ + # https:///api/v1/courses/123/pages?sort=title&order=asc + # + # @returns [Page] + def index + if authorized_action(@context.wiki, @current_user, :read) + pages_route = polymorphic_url([:api_v1, @context, :wiki_pages]) + # omit body from selection, since it's not included in index results + scope = @context.wiki.wiki_pages. + select(%w(wiki_pages.wiki_id wiki_pages.url wiki_pages.title wiki_pages.created_at wiki_pages.updated_at wiki_pages.hide_from_students wiki_pages.editing_roles wiki_pages.workflow_state wiki_pages.user_id)). + includes(:user) + + scope = @context.grants_right?(@current_user, session, :view_unpublished_items) ? scope.not_deleted : scope.active + scope = scope.visible_to_students unless @context.grants_right?(@current_user, session, :view_hidden_items) + + order_clause = case params[:sort] + when 'title' + WikiPage.title_order_by_clause + when 'created_at' + 'wiki_pages.created_at' + when 'updated_at' + 'wiki_pages.updated_at' + else + 'wiki_pages.id' + end + order_clause += ' DESC' if params[:order] == 'desc' + scope = scope.order(order_clause) + + wiki_pages = Api.paginate(scope, self, pages_route) + render :json => wiki_pages_json(wiki_pages, @current_user, session) + end + end + + # @API Show page + # + # Retrieve the content of a wiki page + # + # @argument url the unique identifier for a page. Use 'front-page' to retrieve the front page of the wiki. + # + # @example_request + # curl -H 'Authorization: Bearer ' \ + # https:///api/v1/courses/123/pages/front-page + # + # @returns Page + def show + if authorized_action(@page, @current_user, :read) + @page.increment_view_count(@current_user, @context) + log_asset_access(@page, "wiki", @wiki) + render :json => wiki_page_json(@page, @current_user, session) + end + end + + # @API Create page + # + # Create a new wiki page + # + # @argument wiki_page[title] the title for the new page. + # @argument wiki_page[body] the content for the new page. + # @argument wiki_page[hide_from_students] [boolean] whether the page should be hidden from students. + # @argument wiki_page[notify_of_update] [boolean] whether participants should be notified when this page changes. + # @argument wiki_page[published] [optional] [boolean] whether the page is published (true) or draft state (false). + # + # @example_request + # curl -X POST -H 'Authorization: Bearer ' \ + # https:///api/v1/courses/123/pages?wiki_page[title]=New+page&wiki_page[body]=New+body+text + # + # @returns Page + def create + @page = @context.wiki.wiki_pages.build + if authorized_action(@page, @current_user, :update) + attrs_to_update = process_update_params + if @page.update_attributes(attrs_to_update) + log_asset_access(@page, "wiki", @wiki, 'participate') + render :json => wiki_page_json(@page, @current_user, session) + else + render :json => @page.errors.to_json, :status => :bad_request + end + end + end + + # @API Update page + # + # Update the title or contents of a wiki page + # + # @argument url the unique identifier for a page. + # @argument wiki_page[title] [optional] the new title for the page. + # NOTE: changing a page's title will change its url. The updated url will be returned in the result. + # @argument wiki_page[body] [optional] the new content for the page. + # @argument wiki_page[hide_from_students] [optional] boolean; whether the page should be hidden from students. + # @argument wiki_page[notify_of_update] [optional] [boolean] notify participants that the wiki page has been changed. + # @argument wiki_page[published] [optional] [boolean] whether the page is published (true) or draft state (false) + # + # @example_request + # curl -X PUT -H 'Authorization: Bearer ' \ + # https:///api/v1/courses/123/pages/the-page-url?wiki_page[body]=Updated+body+text + # + # @returns Page + def update + if authorized_action(@page, @current_user, :update_content) + attrs_to_update = process_update_params + if @page.update_attributes(attrs_to_update) + log_asset_access(@page, "wiki", @wiki, 'participate') + @page.context_module_action(@current_user, @context, :contributed) + render :json => wiki_page_json(@page, @current_user, session) + else + render :json => @page.errors.to_json, :status => :bad_request + end + end + end + + # @API Delete page + # + # Delete a wiki page + # + # @argument url the unique identifier for a page. + # + # @example_request + # curl -X DELETE -H 'Authorization: Bearer ' \ + # https:///api/v1/courses/123/pages/the-page-url + # + # @returns Page + def destroy + if authorized_action(@page, @current_user, :delete) + @page.workflow_state = 'deleted' + @page.save! + render :json => wiki_page_json(@page, @current_user, session) + end + end + + protected + + def get_wiki_page + @wiki = @context.wiki + @page = @wiki.wiki_pages.not_deleted.find_by_url!(params[:url]) + end + + def process_update_params + page_params = params[:wiki_page] || {} + + if @page.grants_right?(@current_user, session, :update) + if page_params.has_key? :published + new_state = value_to_boolean(page_params.delete(:published)) ? 'active' : 'unpublished' + @page.workflow_state = new_state + end + + roles = page_params[:editing_roles] + if roles.present? + page_params[:editing_roles] = roles.split(',').map(&:strip).reject{|role| !%w(teachers students public).include?(role)}.join(',') + end + else + # editing_roles only allow changing content, not title or attributes + page_params.slice!(:body) + end + + page_params[:user_id] = @current_user.id if @current_user + + page_params + end + +end diff --git a/app/controllers/wiki_pages_controller.rb b/app/controllers/wiki_pages_controller.rb index 1b2ff00dbc9..ce9e6346bdc 100644 --- a/app/controllers/wiki_pages_controller.rb +++ b/app/controllers/wiki_pages_controller.rb @@ -15,42 +15,12 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # - -# @API Pages -# -# Pages are rich content associated with Courses and Groups in Canvas. -# The Pages API allows this content to be enumerated and retrieved. -# -# @object Page -# { -# // the unique locator for the page -# url: "my-page-title", -# -# // the title of the page -# title: "My Page Title", -# -# // the creation date for the page -# created_at: "2012-08-06T16:46:33-06:00", -# -# // the date the page was last updated -# updated_at: "2012-08-08T14:25:20-06:00", -# -# // whether this page is hidden from students -# // (note: students will never see this true; pages hidden from them will be omitted from results) -# hide_from_students: false, -# -# // the page content, in HTML -# // (present when requesting a single page; omitted when listing pages) -# body: "

Page Content

" -# } class WikiPagesController < ApplicationController before_filter :require_context - before_filter :get_wiki_page, :except => [:index, :api_index, :api_show] + before_filter :get_wiki_page, :except => [:index] add_crumb(proc { t '#crumbs.wiki_pages', "Pages"}) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_wiki_pages_url } before_filter { |c| c.active_tab = "pages" } - include Api::V1::WikiPage - def show @editing = true if Canvas::Plugin.value_to_boolean(params[:edit]) if @page.deleted? && !@page.grants_right?(@current_user, session, :update) && @page.url != 'front-page' @@ -60,7 +30,8 @@ class WikiPagesController < ApplicationController end if is_authorized_action?(@page, @current_user, :read) add_crumb(@page.title) - update_view_count(@page) + @page.increment_view_count(@current_user, @context) + log_asset_access(@page, "wiki", @wiki) respond_to do |format| format.html {render :action => "show" } format.json {render :json => @page.to_json } @@ -75,53 +46,6 @@ class WikiPagesController < ApplicationController redirect_to named_context_url(@context, :context_wiki_page_url, 'front-page') end - # @API List pages - # - # Lists the wiki pages associated with a course or group. - # - # @example_request - # curl -H 'Authorization: Bearer ' \ - # https:///api/v1/courses/123/pages - # - # @returns [Page] - def api_index - if authorized_action(@context.wiki, @current_user, :read) - pages_route = polymorphic_url([:api_v1, @context, :wiki_pages]) - scope = @context.wiki.wiki_pages.order_by_id - if @context.grants_right?(@current_user, session, :view_unpublished_items) - scope = scope.not_deleted - else - scope = scope.active - end - if !@context.grants_right?(@current_user, session, :view_hidden_items) - scope = scope.visible_to_students - end - wiki_pages = Api.paginate(scope, self, pages_route) - render :json => wiki_pages_json(wiki_pages, @current_user, session) - end - end - - # @API Show page - # - # Retrieves the content of a wiki page. - # - # @argument url the unique identifier for a page. Use 'front-page' to retrieve the front page of the wiki. - # - # @example_request - # curl -H 'Authorization: Bearer ' \ - # https:///api/v1/courses/123/pages/front-page - # - # @returns Page - def api_show - # not using get_wiki_page since this API is read-only for now - @wiki = @context.wiki - @page = @wiki.wiki_pages.not_deleted.find_by_url!(params[:url]) - if authorized_action(@page, @current_user, :read) - update_view_count(@page) - render :json => wiki_page_json(@page, @current_user, session) - end - end - def update if authorized_action(@page, @current_user, :update_content) unless @page.grants_right?(@current_user, session, :update) @@ -188,15 +112,5 @@ class WikiPagesController < ApplicationController end res end - - def update_view_count(page) - unless page.new_record? - page.with_versioning(false) do |p| - p.context_module_action(@current_user, @context, :read) - WikiPage.connection.execute("UPDATE wiki_pages SET view_count=COALESCE(view_count, 0) + 1 WHERE id=#{p.id}") - end - log_asset_access(page, "wiki", @wiki) - end - end end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 7d74757f877..b704f6d5875 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -60,6 +60,10 @@ class WikiPage < ActiveRecord::Base end end + def self.title_order_by_clause + best_unicode_collation_key('wiki_pages.title') + end + def ensure_unique_url url_attribute = self.class.url_attribute base_url = self.send(url_attribute) @@ -142,7 +146,7 @@ class WikiPage < ActiveRecord::Base end def notify_of_update=(val) - @wiki_page_changed = (val == '1' || val == true) + @wiki_page_changed = Canvas::Plugin.value_to_boolean(val) end def notify_of_update @@ -495,4 +499,14 @@ class WikiPage < ActiveRecord::Base def self.comments_enabled? !Rails.env.production? end + + def increment_view_count(user, context = nil) + unless self.new_record? + self.with_versioning(false) do |p| + context ||= p.context + p.connection.execute("UPDATE wiki_pages SET view_count=COALESCE(view_count, 0) + 1 WHERE id=#{p.id}") + p.context_module_action(user, context, :read) + end + end + end end diff --git a/config/routes.rb b/config/routes.rb index 7dc1c4d304e..a46da20cb61 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1045,11 +1045,17 @@ ActionController::Routing::Routes.draw do |map| favorites.delete "users/self/favorites/courses", :action => :reset_course_favorites end - api.with_options(:controller => :wiki_pages) do |wiki_pages| - wiki_pages.get "courses/:course_id/pages", :action => :api_index, :path_name => 'course_wiki_pages' - wiki_pages.get "groups/:group_id/pages", :action => :api_index, :path_name => 'group_wiki_pages' - wiki_pages.get "courses/:course_id/pages/:url", :action => :api_show, :path_name => 'course_wiki_page' - wiki_pages.get "groups/:group_id/pages/:url", :action => :api_show, :path_name => 'group_wiki_page' + api.with_options(:controller => :wiki_pages_api) do |wiki_pages| + wiki_pages.get "courses/:course_id/pages", :action => :index, :path_name => 'course_wiki_pages' + wiki_pages.get "groups/:group_id/pages", :action => :index, :path_name => 'group_wiki_pages' + wiki_pages.get "courses/:course_id/pages/:url", :action => :show, :path_name => 'course_wiki_page' + wiki_pages.get "groups/:group_id/pages/:url", :action => :show, :path_name => 'group_wiki_page' + wiki_pages.post "courses/:course_id/pages", :action => :create + wiki_pages.post "groups/:group_id/pages", :action => :create + wiki_pages.put "courses/:course_id/pages/:url", :action => :update + wiki_pages.put "groups/:group_id/pages/:url", :action => :update + wiki_pages.delete "courses/:course_id/pages/:url", :action => :destroy + wiki_pages.delete "groups/:group_id/pages/:url", :action => :destroy end api.with_options(:controller => :context_modules_api) do |context_modules| diff --git a/lib/api/v1/wiki_page.rb b/lib/api/v1/wiki_page.rb index fa9d0e2fed9..a34558b3b6b 100644 --- a/lib/api/v1/wiki_page.rb +++ b/lib/api/v1/wiki_page.rb @@ -20,15 +20,18 @@ module Api::V1::WikiPage include Api::V1::Json include Api::V1::User - WIKI_PAGE_JSON_ATTRS = %w(url title created_at updated_at hide_from_students) + WIKI_PAGE_JSON_ATTRS = %w(url title created_at updated_at hide_from_students editing_roles) - def wiki_page_json(wiki_page, current_user, session) + def wiki_page_json(wiki_page, current_user, session, include_body = true) hash = api_json(wiki_page, current_user, session, :only => WIKI_PAGE_JSON_ATTRS) - hash['body'] = api_user_content(wiki_page.body) + hash['editing_roles'] ||= 'teachers' + hash['body'] = api_user_content(wiki_page.body) if include_body + hash['last_edited_by'] = user_display_json(wiki_page.user, wiki_page.context) if wiki_page.user + hash['published'] = wiki_page.active? hash end def wiki_pages_json(wiki_pages, current_user, session) - wiki_pages.map { |page| api_json(page, current_user, session, :only => WIKI_PAGE_JSON_ATTRS) } + wiki_pages.map { |page| wiki_page_json(page, current_user, session, false) } end end diff --git a/spec/apis/user_content_spec.rb b/spec/apis/user_content_spec.rb index 6d08772a55f..42a03946f69 100644 --- a/spec/apis/user_content_spec.rb +++ b/spec/apis/user_content_spec.rb @@ -218,7 +218,7 @@ describe UserContent, :type => :integration do @wiki_page.workflow_state = 'active' @wiki_page.save! api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@wiki_page.url}", - { :controller => 'wiki_pages', :action => 'api_show', + { :controller => 'wiki_pages_api', :action => 'show', :format => 'json', :course_id => @course.id.to_s, :url => @wiki_page.url }) end @@ -244,7 +244,7 @@ describe UserContent, :type => :integration do @wiki_page.save! json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@wiki_page.url}", - { :controller => 'wiki_pages', :action => 'api_show', + { :controller => 'wiki_pages_api', :action => 'show', :format => 'json', :course_id => @course.id.to_s, :url => @wiki_page.url }) doc = Nokogiri::HTML::DocumentFragment.parse(json['body']) doc.css('a').collect { |att| att['data-api-endpoint'] }.should == [ @@ -281,7 +281,7 @@ describe UserContent, :type => :integration do @wiki_page.save! json = api_call(:get, "/api/v1/groups/#{@group.id}/pages/#{@wiki_page.url}", - { :controller => 'wiki_pages', :action => 'api_show', + { :controller => 'wiki_pages_api', :action => 'show', :format => 'json', :group_id => @group.id.to_s, :url => @wiki_page.url }) doc = Nokogiri::HTML::DocumentFragment.parse(json['body']) doc.css('a').collect { |att| att['data-api-endpoint'] }.should == [ diff --git a/spec/apis/v1/pages_api_spec.rb b/spec/apis/v1/pages_api_spec.rb index bc5ecf64b3d..996fbb0537f 100644 --- a/spec/apis/v1/pages_api_spec.rb +++ b/spec/apis/v1/pages_api_spec.rb @@ -33,51 +33,231 @@ describe "Pages API", :type => :integration do course_with_teacher(:course => @course, :active_all => true) end - it "should list pages, including hidden ones" do - json = api_call(:get, "/api/v1/courses/#{@course.id}/pages", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}") - json.should == [{"hide_from_students" => false, "url" => @front_page.url, "created_at" => @front_page.created_at.as_json, "updated_at" => @front_page.updated_at.as_json, "title" => @front_page.title}, - {"hide_from_students" => true, "url" => @hidden_page.url, "created_at" => @hidden_page.created_at.as_json, "updated_at" => @hidden_page.updated_at.as_json, "title" => @hidden_page.title}] + describe "index" do + it "should list pages, including hidden ones" do + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param) + json.map {|entry| entry.slice(*%w(hide_from_students url created_at updated_at title))}.should == + [{"hide_from_students" => false, "url" => @front_page.url, "created_at" => @front_page.created_at.as_json, "updated_at" => @front_page.updated_at.as_json, "title" => @front_page.title}, + {"hide_from_students" => true, "url" => @hidden_page.url, "created_at" => @hidden_page.created_at.as_json, "updated_at" => @hidden_page.updated_at.as_json, "title" => @hidden_page.title}] + end + + it "should paginate" do + 2.times { |i| @wiki.wiki_pages.create!(:title => "New Page #{i}") } + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?per_page=2", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param, :per_page => "2") + json.size.should == 2 + urls = json.collect{ |page| page['url'] } + + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?per_page=2&page=2", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param, :per_page => "2", :page => "2") + json.size.should == 2 + urls += json.collect{ |page| page['url'] } + + urls.should == @wiki.wiki_pages.sort_by(&:id).collect(&:url) + end + + describe "sorting" do + it "should sort by title (case-insensitive)" do + @wiki.wiki_pages.create! :title => 'gIntermediate Page' + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?sort=title", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param, + :sort=>'title') + json.map {|page|page['title']}.should == ['Front Page', 'gIntermediate Page', 'Hidden Page'] + + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?sort=title&order=desc", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param, + :sort=>'title', :order=>'desc') + json.map {|page|page['title']}.should == ['Hidden Page', 'gIntermediate Page', 'Front Page'] + end + + it "should sort by created_at" do + @hidden_page.update_attribute(:created_at, 1.hour.ago) + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?sort=created_at&order=asc", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param, + :sort=>'created_at', :order=>'asc') + json.map {|page|page['url']}.should == [@hidden_page.url, @front_page.url] + end + + it "should sort by updated_at" do + Timecop.freeze(1.hour.ago) { @hidden_page.touch } + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?sort=updated_at&order=desc", + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>@course.to_param, + :sort=>'updated_at', :order=>'desc') + json.map {|page|page['url']}.should == [@front_page.url, @hidden_page.url] + end + end + 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! + @hidden_page.user_id = @teacher.id + @hidden_page.save! + end + + it "should retrieve page content and attributes" do + json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", + :controller=>"wiki_pages_api", :action=>"show", :format=>"json", :course_id=>"#{@course.id}", :url=>@hidden_page.url) + expected = { "hide_from_students" => true, + "editing_roles" => "teachers", + "last_edited_by" => user_display_json(@teacher, @course).stringify_keys!, + "url" => @hidden_page.url, + "created_at" => @hidden_page.created_at.as_json, + "updated_at" => @hidden_page.updated_at.as_json, + "title" => @hidden_page.title, + "body" => @hidden_page.body, + "published" => true } + json.should == expected + end + end + + describe "create" do + it "should require a title" do + api_call(:post, "/api/v1/courses/#{@course.id}/pages", + { :controller => 'wiki_pages_api', :action => 'create', :format => 'json', :course_id => @course.to_param }, + {}, {}, {:expected_status => 400}) + end + + it "should create a new page" do + json = api_call(:post, "/api/v1/courses/#{@course.id}/pages", + { :controller => 'wiki_pages_api', :action => 'create', :format => 'json', :course_id => @course.to_param }, + { :wiki_page => { :title => 'New Wiki Page!', :body => 'hello new page' }}) + page = @course.wiki.wiki_pages.find_by_url!(json['url']) + page.title.should == 'New Wiki Page!' + page.url.should == 'new-wiki-page' + page.body.should == 'hello new page' + page.user_id.should == @teacher.id + end + + it "should create a new page in published state" do + json = api_call(:post, "/api/v1/courses/#{@course.id}/pages", + { :controller => 'wiki_pages_api', :action => 'create', :format => 'json', :course_id => @course.to_param }, + { :wiki_page => { :published => true, :title => 'New Wiki Page!', :body => 'hello new page' }}) + page = @course.wiki.wiki_pages.find_by_url!(json['url']) + page.should be_active + json['published'].should be_true + end + + it "should create a new page in unpublished state" do + json = api_call(:post, "/api/v1/courses/#{@course.id}/pages", + { :controller => 'wiki_pages_api', :action => 'create', :format => 'json', :course_id => @course.to_param }, + { :wiki_page => { :published => false, :title => 'New Wiki Page!', :body => 'hello new page' }}) + page = @course.wiki.wiki_pages.find_by_url!(json['url']) + page.should be_unpublished + json['published'].should be_false + end end - it "should paginate" do - 2.times { |i| @wiki.wiki_pages.create!(:title => "New Page #{i}") } - json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?per_page=2", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}", :per_page=>"2") - json.size.should == 2 - urls = json.collect{ |page| page['url'] } + describe "update" do + it "should update page content and attributes" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", + { :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url }, + { :wiki_page => { :title => 'No Longer Hidden Page', :hide_from_students => false, + :body => 'Information wants to be free' }}) + @hidden_page.reload + @hidden_page.should be_active + @hidden_page.hide_from_students.should be_false + @hidden_page.title.should == 'No Longer Hidden Page' + @hidden_page.body.should == 'Information wants to be free' + @hidden_page.user_id.should == @teacher.id + end + + context "with unpublished page" do + before do + @hidden_page.workflow_state = 'unpublished' + @hidden_page.save! + end + + it "should publish a page with published=true" do + json = api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}?wiki_page[published]=true", + :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url, :wiki_page => {'published' => 'true'}) + json['published'].should be_true + @hidden_page.reload.should be_active + end + + it "should not publish a page otherwise" do + json = api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", + :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url) + json['published'].should be_false + @hidden_page.reload.should be_unpublished + end + end + + it "should unpublish a page" do + json = api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}?wiki_page[published]=false", + :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url, :wiki_page => {'published' => 'false'}) + json['published'].should be_false + @hidden_page.reload.should be_unpublished + end + + it "should sanitize page content" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", + { :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url }, + { :wiki_page => { :body => "

lolcats

" }}) + @hidden_page.reload + @hidden_page.body.should == "

lolcats

alert('what')" + end - json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?page=2&per_page=2", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}", :page => "2", :per_page=>"2") - json.size.should == 2 - urls += json.collect{ |page| page['url'] } + it "should clean editing_roles" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", + { :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url }, + { :wiki_page => { :editing_roles => 'teachers, chimpanzees, students' }}) + @hidden_page.reload + @hidden_page.editing_roles.should == 'teachers,students' + end - urls.should == @wiki.wiki_pages.sort_by(&:id).collect(&:url) + it "should 404 if the page doesn't exist" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/nonexistent-url?title=renamed", + { :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => 'nonexistent-url', :title => 'renamed' }, {}, {}, { :expected_status => 404 }) + end + + describe "notify_of_update" do + before do + @front_page.update_attribute(:created_at, 1.hour.ago) + @hidden_page.update_attribute(:created_at, 1.hour.ago) + @notification = Notification.create! :name => "Updated Wiki Page" + @teacher.communication_channels.create(:path => "teacher@instructure.com").confirm! + @teacher.email_channel.notification_policies. + find_or_create_by_notification_id(@notification.id). + update_attribute(:frequency, 'immediately') + end + + it "should notify iff the notify_on_update flag is sent" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@front_page.url}?wiki_page[body]=updated+front+page", + :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @front_page.url, :wiki_page => { "body" => "updated front page" }) + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}?wiki_page[body]=updated+hidden+page&wiki_page[notify_of_update]=true", + :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url, :wiki_page => { "body" => "updated hidden page", "notify_of_update" => 'true' }) + @teacher.messages.map(&:context_id).should == [@hidden_page.id] + end + end end - it "should retrieve page content" do - json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", - :controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{@course.id}", :url=>@hidden_page.url) - json.should == { "hide_from_students" => true, - "url" => @hidden_page.url, - "created_at" => @hidden_page.created_at.as_json, - "updated_at" => @hidden_page.updated_at.as_json, - "title" => @hidden_page.title, - "body" => @hidden_page.body } - end - - it "should update view count" do - views = @front_page.view_count - api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@front_page.url}", - :controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{@course.id}", :url=>@front_page.url) - @front_page.reload - @front_page.view_count.should == views + 1 - end - - it "should return not-found on a nonexistent page" do - api_call(:get, "/api/v1/courses/#{@course.id}/pages/nonexistent", - { :controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{@course.id}", :url=>'nonexistent' }, - {}, {}, { :expected_status => 404 }) + describe "delete" do + it "should delete a page" do + api_call(:delete, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", + { :controller => 'wiki_pages_api', :action => 'destroy', :format => 'json', :course_id => @course.to_param, + :url => @hidden_page.url }) + @hidden_page.reload.should be_deleted + end end context "unpublished pages" do @@ -89,12 +269,12 @@ describe "Pages API", :type => :integration do it "should be in index" do json = api_call(:get, "/api/v1/courses/#{@course.id}/pages", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}") + :controller=>"wiki_pages_api", :action=>"index", :format=>"json", :course_id=>"#{@course.id}") json.select{|w|w[:title] == @unpublished_page.title}.should_not be_nil end it "should show" do json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@unpublished_page.url}", - :controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{@course.id}", :url=>@unpublished_page.url) + :controller=>"wiki_pages_api", :action=>"show", :format=>"json", :course_id=>"#{@course.id}", :url=>@unpublished_page.url) json['title'].should == @unpublished_page.title end end @@ -107,19 +287,20 @@ describe "Pages API", :type => :integration do it "should list pages, excluding hidden ones" do json = api_call(:get, "/api/v1/courses/#{@course.id}/pages", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}") - json.should == [{"hide_from_students" => false, "url" => @front_page.url, "created_at" => @front_page.created_at.as_json, "updated_at" => @front_page.updated_at.as_json, "title" => @front_page.title}] + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{@course.id}") + json.map{|entry| entry.slice(*%w(hide_from_students url created_at updated_at title))}.should == + [{"hide_from_students" => false, "url" => @front_page.url, "created_at" => @front_page.created_at.as_json, "updated_at" => @front_page.updated_at.as_json, "title" => @front_page.title}] end - + it "should paginate, excluding hidden" do 11.times { |i| @wiki.wiki_pages.create!(:title => "New Page #{i}") } json = api_call(:get, "/api/v1/courses/#{@course.id}/pages", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}") + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{@course.id}") json.size.should == 10 urls = json.collect{ |page| page['url'] } json = api_call(:get, "/api/v1/courses/#{@course.id}/pages?page=2", - :controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}", :page => "2") + :controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{@course.id}", :page => "2") json.size.should == 2 urls += json.collect{ |page| page['url'] } @@ -128,15 +309,15 @@ describe "Pages API", :type => :integration do it "should refuse to show a hidden page" do api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@hidden_page.url}", - {:controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{@course.id}", :url=>@hidden_page.url}, - {}, {}, { :expected_status => 401 }) + {:controller=>'wiki_pages_api', :action=>'show', :format=>'json', :course_id=>"#{@course.id}", :url=>@hidden_page.url}, + {}, {}, { :expected_status => 401 }) end it "should refuse to list pages in an unpublished course" do @course.workflow_state = 'created' @course.save! api_call(:get, "/api/v1/courses/#{@course.id}/pages", - {:controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}"}, + {:controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{@course.id}"}, {}, {}, { :expected_status => 401 }) end @@ -149,11 +330,11 @@ describe "Pages API", :type => :integration do other_page.save! api_call(:get, "/api/v1/courses/#{other_course.id}/pages", - {:controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{other_course.id}"}, + {:controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{other_course.id}"}, {}, {}, { :expected_status => 401 }) api_call(:get, "/api/v1/courses/#{other_course.id}/pages/front-page", - {:controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{other_course.id}", :url=>'front-page'}, + {:controller=>'wiki_pages_api', :action=>'show', :format=>'json', :course_id=>"#{other_course.id}", :url=>'front-page'}, {}, {}, { :expected_status => 401 }) end @@ -166,11 +347,12 @@ describe "Pages API", :type => :integration do other_page.workflow_state = 'active' other_page.save! - api_call(:get, "/api/v1/courses/#{other_course.id}/pages", - {:controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{other_course.id}"}) + json = api_call(:get, "/api/v1/courses/#{other_course.id}/pages", + {:controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{other_course.id}"}) + json.should_not be_empty api_call(:get, "/api/v1/courses/#{other_course.id}/pages/front-page", - {:controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{other_course.id}", :url=>'front-page'}) + {:controller=>'wiki_pages_api', :action=>'show', :format=>'json', :course_id=>"#{other_course.id}", :url=>'front-page'}) end it "should fulfill module progression requirements" do @@ -181,13 +363,65 @@ describe "Pages API", :type => :integration do # index should not affect anything api_call(:get, "/api/v1/courses/#{@course.id}/pages", - {:controller=>"wiki_pages", :action=>"api_index", :format=>"json", :course_id=>"#{@course.id}"}) + {:controller=>'wiki_pages_api', :action=>'index', :format=>'json', :course_id=>"#{@course.id}"}) mod.evaluate_for(@user).workflow_state.should == "unlocked" # show should count as a view api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@front_page.url}", - {:controller=>"wiki_pages", :action=>"api_show", :format=>"json", :course_id=>"#{@course.id}", :url=>@front_page.url}) - mod.evaluate_for(@user).workflow_state.should == "completed" + {:controller=>'wiki_pages_api', :action=>'show', :format=>'json', :course_id=>"#{@course.id}", :url=>@front_page.url}) + mod.evaluate_for(@user).workflow_state.should == "completed" + end + + it "should not allow editing a page" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@front_page.url}", + { :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @front_page.url }, + { :publish => false, :wiki_page => { :body => '!!!!' }}, {}, {:expected_status => 401}) + @front_page.reload.body.should_not == '!!!!' + end + + describe "with students in editing_roles" do + before do + @editable_page = @course.wiki.wiki_pages.create! :title => 'Editable Page', :editing_roles => 'students' + @editable_page.workflow_state = 'active' + @editable_page.save! + end + + it "should allow editing the body, but not attributes" do + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@editable_page.url}", + { :controller => 'wiki_pages_api', :action => 'update', :format => 'json', :course_id => @course.to_param, + :url => @editable_page.url }, + { :wiki_page => { :published => false, :title => 'Broken Links', :body => '?!?!' }}) + @editable_page.reload + @editable_page.should be_active + @editable_page.title.should == 'Editable Page' + @editable_page.body.should == '?!?!' + @editable_page.user_id.should == @student.id + end + + it "should fulfill module completion requirements" do + mod = @course.context_modules.create!(:name => "some module") + tag = mod.add_item(:id => @editable_page.id, :type => 'wiki_page') + mod.completion_requirements = { tag.id => {:type => 'must_contribute'} } + mod.save! + + api_call(:put, "/api/v1/courses/#{@course.id}/pages/#{@editable_page.url}", + {:controller=>'wiki_pages_api', :action=>'update', :format=>'json', :course_id=>"#{@course.id}", + :url=>@editable_page.url}, { :wiki_page => { :body => 'edited by student' }}) + mod.evaluate_for(@user).workflow_state.should == "completed" + end + + it "should not allow creating pages" do + api_call(:post, "/api/v1/courses/#{@course.id}/pages", + { :controller => 'wiki_pages_api', :action => 'create', :format => 'json', :course_id => @course.to_param }, + {}, {}, {:expected_status => 401}) + end + + it "should not allow deleting pages" do + api_call(:delete, "/api/v1/courses/#{@course.id}/pages/#{@editable_page.url}", + { :controller => 'wiki_pages_api', :action => 'destroy', :format => 'json', :course_id => @course.to_param, + :url => @editable_page.url }, {}, {}, {:expected_status => 401}) + end end context "unpublished pages" do @@ -199,13 +433,13 @@ describe "Pages API", :type => :integration do it "should not be in index" do json = api_call(:get, "/api/v1/courses/#{@course.id}/pages", - :controller => "wiki_pages", :action => "api_index", :format => "json", :course_id => "#{@course.id}") + :controller => "wiki_pages_api", :action => "index", :format => "json", :course_id => "#{@course.id}") json.select { |w| w[:title] == @unpublished_page.title }.should == [] end it "should not show" do json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@unpublished_page.url}", - {:controller => "wiki_pages", :action => "api_show", :format => "json", :course_id => "#{@course.id}", :url => @unpublished_page.url}, + {:controller => "wiki_pages_api", :action => "show", :format => "json", :course_id => "#{@course.id}", :url => @unpublished_page.url}, {}, {}, {:expected_status => 401}) end @@ -213,7 +447,7 @@ describe "Pages API", :type => :integration do @course.is_public = true @course.save! json = api_call(:get, "/api/v1/courses/#{@course.id}/pages/#{@unpublished_page.url}", - {:controller => "wiki_pages", :action => "api_show", :format => "json", :course_id => "#{@course.id}", :url => @unpublished_page.url}, + {:controller => "wiki_pages_api", :action => "show", :format => "json", :course_id => "#{@course.id}", :url => @unpublished_page.url}, {}, {}, {:expected_status => 401}) end end @@ -227,15 +461,37 @@ describe "Pages API", :type => :integration do it "should list the contents of a group wiki" do json = api_call(:get, "/api/v1/groups/#{@group.id}/pages", - {:controller=>"wiki_pages", :action=>"api_index", :format=>"json", :group_id=>"#{@group.id}"}) + {:controller=>'wiki_pages_api', :action=>'index', :format=>'json', :group_id=>@group.to_param}) json.collect { |row| row['title'] }.should == @group.wiki.wiki_pages.active.order_by_id.collect(&:title) end it "should retrieve page content from a group wiki" do testpage = @group.wiki.wiki_pages.last json = api_call(:get, "/api/v1/groups/#{@group.id}/pages/#{testpage.url}", - {:controller=>"wiki_pages", :action=>"api_show", :format=>"json", :group_id=>"#{@group.id}", :url=>testpage.url}) + {:controller=>'wiki_pages_api', :action=>'show', :format=>'json', :group_id=>@group.to_param, :url=>testpage.url}) json['body'].should == testpage.body end + + it "should create a group wiki page" do + json = api_call(:post, "/api/v1/groups/#{@group.id}/pages?wiki_page[title]=newpage", + {:controller=>'wiki_pages_api', :action=>'create', :format=>'json', :group_id=>@group.to_param, :wiki_page => {'title' => 'newpage'}}) + page = @group.wiki.wiki_pages.find_by_url!(json['url']) + page.title.should == 'newpage' + end + + it "should update a group wiki page" do + testpage = @group.wiki.wiki_pages.first + api_call(:put, "/api/v1/groups/#{@group.id}/pages/#{testpage.url}?wiki_page[body]=lolcats", + {:controller=>'wiki_pages_api', :action=>'update', :format=>'json', :group_id=>@group.to_param, :url=>testpage.url, :wiki_page => {'body' => 'lolcats'}}) + testpage.reload.body.should == 'lolcats' + end + + it "should delete a group wiki page" do + count = @group.wiki.wiki_pages.not_deleted.size + testpage = @group.wiki.wiki_pages.last + api_call(:delete, "/api/v1/groups/#{@group.id}/pages/#{testpage.url}", + {: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 end end