start discussion topic api
refs #4752 Change-Id: I54712c27ef1c496c2c92f7805240ca91405b2858 Reviewed-on: https://gerrit.instructure.com/4797 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
parent
d97df4ef7e
commit
b3167f4640
|
@ -16,24 +16,85 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# @API Discussion Topics
|
||||
#
|
||||
# API for accessing and participating in discussion topics in groups and courses.
|
||||
class DiscussionTopicsController < ApplicationController
|
||||
before_filter :require_context, :except => :public_feed
|
||||
|
||||
add_crumb(lambda { t('#crumbs.discussions', "Discussions")}, :except => [:public_feed]) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_discussion_topics_url }
|
||||
before_filter { |c| c.active_tab = "discussions" }
|
||||
|
||||
|
||||
include Api::V1::DiscussionTopics
|
||||
|
||||
# @API
|
||||
#
|
||||
# Returns the list of discussion topics for this course.
|
||||
#
|
||||
# @response_field assignment_id The unique identifier of the assignment if the topic is for grading, otherwise null
|
||||
# @response_field attachments Array of attachments
|
||||
# @response_field delayed_post_at The datetime to post the topic (if not right away)
|
||||
# @response_field discussion_subentry_count The count of entries in the topic
|
||||
# @response_field id The unique identifier for the discussion topic.
|
||||
# @response_field last_reply_at The datetime for when the last reply was in the topic
|
||||
# @response_field message The HTML content of the topic
|
||||
# @response_field permissions A hash of permissions for the current user. If a key is not present then the permission is false.
|
||||
# Example:
|
||||
# {"delete"=>true,"reply"=>true,"read"=>true,"attach"=>true,"create"=>true,"update"=>true}
|
||||
# @response_field podcast_url If the topic is a podcast topic this is the feed url for the current user
|
||||
# @response_field posted_at The datetime the topic was posted. If it is null it hasn't been posted yet. (see delayed_post_at)
|
||||
# @response_field require_initial_post If true then a user may not respond to other replies until that user has made an initial reply
|
||||
# @response_field root_topic_id If the topic is for grading and a group assignment this will point to the original topic in the course
|
||||
# @response_field title The title of the topic
|
||||
# @response_field topic_children An array of topic_ids for the group discussions the user is a part of
|
||||
# @response_field user_name The username of the creator
|
||||
#
|
||||
# @example_response
|
||||
# [
|
||||
# {
|
||||
# "id":1,
|
||||
# "title":"Topic 1",
|
||||
# "message":"<p>content here</p>",
|
||||
# "posted_at":"2037-07-21T13:29:31Z",
|
||||
# "last_reply_at":"2037-07-28T19:38:31Z",
|
||||
# "require_initial_post":null,
|
||||
# "discussion_subentry_count":0,
|
||||
# "assignment_id":null,
|
||||
# "delayed_post_at":null,
|
||||
# "user_name":"User Name",
|
||||
# "permissions":{"reply":true,"read":true},
|
||||
# "topic_children":[],
|
||||
# "root_topic_id":null,
|
||||
# "podcast_url":"/feeds/topics/1/enrollment_1XAcepje4u228rt4mi7Z1oFbRpn3RAkTzuXIGOPe.rss",
|
||||
# "attachments":[
|
||||
# {
|
||||
# "content-type":"unknown/unknown",
|
||||
# "url":"http://www.example.com/courses/1/files/1/download",
|
||||
# "filename":"content.txt",
|
||||
# "display_name":"content.txt"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
def index
|
||||
@context.assert_assignment_group rescue nil
|
||||
@all_topics = @context.discussion_topics.active
|
||||
@all_topics = @all_topics.only_discussion_topics if params[:include_announcements] != "1"
|
||||
@topics = @all_topics.paginate(:page => params[:page]).reject{|a| a.locked_for?(@current_user, :check_policies => true) }
|
||||
@topics = @all_topics.paginate(:page => params[:page], :per_page => params[:per_page] || 10).reject{|a| a.locked_for?(@current_user, :check_policies => true) }
|
||||
if authorized_action(@context.discussion_topics.new, @current_user, :read)
|
||||
return child_topic if params[:root_discussion_topic_id] && @context.respond_to?(:context) && @context.context && @context.context.discussion_topics.find(params[:root_discussion_topic_id])
|
||||
log_asset_access("topics:#{@context.asset_string}", "topics", 'other')
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.xml { render :xml => @topics.to_xml }
|
||||
format.json { render :json => @topics.to_json(:methods => [:user_name, :discussion_subentry_count], :permissions => {:user => @current_user, :session => session }) }
|
||||
format.json do
|
||||
if api_request?
|
||||
api_pagination_headers(@topics)
|
||||
render :json => discussion_topic_api_json(@topics)
|
||||
else
|
||||
render :json => @topics.to_json(:methods => [:user_name, :discussion_subentry_count], :permissions => {:user => @current_user, :session => session })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
# id in these URLs is the id of the student in the course, there is no separate
|
||||
# submission id exposed in these APIs.
|
||||
class SubmissionsApiController < ApplicationController
|
||||
include Api
|
||||
before_filter :require_context
|
||||
|
||||
# @API
|
||||
|
@ -249,7 +250,7 @@ class SubmissionsApiController < ApplicationController
|
|||
sc.media_comment_type)
|
||||
end
|
||||
sc_hash['attachments'] = sc.attachments.map do |a|
|
||||
attachment_json(a, assignment, {:comment_id => sc.id, :id => submission.user_id})
|
||||
attachment_json(a, :assignment => assignment, :url_params => {:comment_id => sc.id, :id => submission.user_id})
|
||||
end unless sc.attachments.blank?
|
||||
sc_hash
|
||||
end
|
||||
|
@ -287,7 +288,7 @@ class SubmissionsApiController < ApplicationController
|
|||
attachments = attempt.versioned_attachments.dup
|
||||
attachments << attempt.attachment if attempt.attachment && attempt.attachment.context_type == 'Submission' && attempt.attachment.context_id == attempt.id
|
||||
hash['attachments'] = attachments.map do |attachment|
|
||||
attachment_json(attachment, assignment, :id => attempt.user_id)
|
||||
attachment_json(attachment, :assignment => assignment, :url_params => {:id => attempt.user_id})
|
||||
end unless attachments.blank?
|
||||
|
||||
# include the discussion topic entries
|
||||
|
@ -308,7 +309,7 @@ class SubmissionsApiController < ApplicationController
|
|||
)
|
||||
attachments = (entry.attachments.dup + [entry.attachment]).compact
|
||||
ehash['attachments'] = attachments.map do |attachment|
|
||||
attachment_json(attachment, assignment, :id => attempt.user_id)
|
||||
attachment_json(attachment, :assignment => assignment, :url_params => {:id => attempt.user_id})
|
||||
end unless attachments.blank?
|
||||
ehash
|
||||
end
|
||||
|
@ -317,22 +318,6 @@ class SubmissionsApiController < ApplicationController
|
|||
hash
|
||||
end
|
||||
|
||||
def attachment_json(attachment, assignment, url_params = {})
|
||||
url = case attachment.context_type
|
||||
when "Course"
|
||||
course_file_download_url(url_params.merge(:file_id => attachment.id, :id => nil))
|
||||
else
|
||||
course_assignment_submission_url(@context, assignment,
|
||||
url_params.merge(:download => attachment.id))
|
||||
end
|
||||
{
|
||||
'content-type' => attachment.content_type,
|
||||
'display_name' => attachment.display_name,
|
||||
'filename' => attachment.filename,
|
||||
'url' => url,
|
||||
}
|
||||
end
|
||||
|
||||
# a media comment looks just like an attachment to the API
|
||||
def media_comment_json(media_comment_id, media_comment_type)
|
||||
{
|
||||
|
|
|
@ -664,6 +664,14 @@ ActionController::Routing::Routes.draw do |map|
|
|||
course.student_submissions 'students/submissions.:format',
|
||||
:controller => 'submissions_api', :action => 'for_students',
|
||||
:conditions => { :method => :get }
|
||||
course.resources :discussion_topics,
|
||||
:only => %w(index)
|
||||
end
|
||||
api.resources :groups,
|
||||
:name_prefix => "api_v1_",
|
||||
:only => %w() do |group|
|
||||
group.resources :discussion_topics,
|
||||
:only => %w(index)
|
||||
end
|
||||
api.resources :accounts, :only => %{} do |account|
|
||||
account.resources :sis_imports,
|
||||
|
|
|
@ -146,3 +146,21 @@ the sis field, like "sis\_course\_id:". For instance, to retrieve the
|
|||
list of assignments for a course with sis id of 'A1234':
|
||||
|
||||
/api/v1/courses/sis_course_id:A1234/assignments.json
|
||||
|
||||
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.
|
||||
|
||||
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"
|
||||
|
||||
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.
|
||||
|
|
41
lib/api.rb
41
lib/api.rb
|
@ -67,6 +67,47 @@ module Api
|
|||
raise ArgumentError, "need to add support for table name: #{collection.table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
# Add [link HTTP Headers](http://www.w3.org/Protocols/9707-link-header.html) for pagination
|
||||
# The collection needs to be a will_paginate collection
|
||||
def self.set_pagination_headers!(collection, response, base_url)
|
||||
return unless collection.respond_to?(:next_page)
|
||||
links = []
|
||||
template = "<#{base_url}.json?page=%s&per_page=#{collection.per_page}>; rel=\"%s\""
|
||||
if collection.next_page
|
||||
links << template % [collection.next_page, "next"]
|
||||
end
|
||||
if collection.previous_page
|
||||
links << template % [collection.previous_page, "prev"]
|
||||
end
|
||||
if collection.total_pages > 1
|
||||
links << template % [1, "first"]
|
||||
links << template % [collection.total_pages, "last"]
|
||||
end
|
||||
response.headers["Link"] = links.join(',') if links.length > 0
|
||||
end
|
||||
|
||||
def attachment_json(attachment, opts={})
|
||||
url_params = opts[:url_params] || {}
|
||||
|
||||
url = case attachment.context_type
|
||||
when "Course"
|
||||
course_file_download_url(url_params.merge(:file_id => attachment.id, :id => nil))
|
||||
when "Group"
|
||||
group_file_download_url(url_params.merge(:file_id => attachment.id, :id => nil))
|
||||
when /Submission|User/
|
||||
return nil unless opts[:assignment]
|
||||
course_assignment_submission_url(@context, opts[:assignment], url_params.merge(:download => attachment.id))
|
||||
else
|
||||
return nil
|
||||
end
|
||||
{
|
||||
'content-type' => attachment.content_type,
|
||||
'display_name' => attachment.display_name,
|
||||
'filename' => attachment.filename,
|
||||
'url' => url,
|
||||
}
|
||||
end
|
||||
|
||||
# See User.submissions_for_given_assignments and SubmissionsApiController#for_students
|
||||
mattr_accessor :assignment_ids_for_students_api
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module Api::V1::DiscussionTopics
|
||||
include Api
|
||||
|
||||
def discussion_topic_api_json(topics)
|
||||
topics.map do |topic|
|
||||
|
||||
attachments = []
|
||||
if topic.attachment
|
||||
attachments << attachment_json(topic.attachment, :context => @context)
|
||||
end
|
||||
|
||||
url = nil
|
||||
if topic.podcast_enabled
|
||||
code = @context_enrollment ? @context_enrollment.feed_code : @context.feed_code
|
||||
url = feeds_topic_format_path(topic.id, code, :rss)
|
||||
end
|
||||
|
||||
children = topic.child_topics.scoped(:select => 'id').map(&:id)
|
||||
|
||||
topic.as_json(:include_root => false,
|
||||
:only => %w(id title assignment_id delayed_post_at last_reply_at message posted_at require_initial_post root_topic_id),
|
||||
:methods => [:user_name, :discussion_subentry_count],
|
||||
:permissions => {:user => @current_user, :session => session}
|
||||
).tap do |json|
|
||||
json.merge! :podcast_url => url,
|
||||
:topic_children => children,
|
||||
:attachments => attachments
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def api_pagination_headers(topics)
|
||||
if @context.is_a? Course
|
||||
Api.set_pagination_headers!(topics, response, api_v1_course_discussion_topics_path(@context))
|
||||
else
|
||||
Api.set_pagination_headers!(topics, response, api_v1_group_discussion_topics_path(@context))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,136 @@
|
|||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
|
||||
|
||||
describe DiscussionTopicsController, :type => :integration do
|
||||
before(:each) do
|
||||
course_with_teacher(:active_all => true, :user => user_with_pseudonym)
|
||||
end
|
||||
|
||||
it "should return discussion topic list" do
|
||||
@topic = @course.discussion_topics.create!(:title => "Topic 1", :message => "<p>content here</p>", :podcast_enabled => true)
|
||||
att = Attachment.create!(:filename => 'content.txt', :display_name => "content.txt", :uploaded_data => StringIO.new('attachment content'), :folder => Folder.unfiled_folder(@course), :context => @course)
|
||||
@topic.attachment = att
|
||||
@topic.save
|
||||
|
||||
sub = @course.discussion_topics.create!(:title => "Sub topic", :message => "<p>i'm subversive</p>")
|
||||
sub.root_topic_id = @topic.id
|
||||
sub.save
|
||||
|
||||
json = api_call(:get, "/api/v1/courses/#{@course.id}/discussion_topics.json",
|
||||
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :course_id => @course.id.to_s})
|
||||
|
||||
# get rid of random characters in podcast url
|
||||
json.last["podcast_url"].gsub!(/_[^.]*/, '_randomness')
|
||||
json.last == {"podcast_url"=>"/feeds/topics/1/enrollment_randomness.rss",
|
||||
"require_initial_post"=>nil,
|
||||
"title"=>"Topic 1",
|
||||
"discussion_subentry_count"=>0,
|
||||
"assignment_id"=>nil,
|
||||
"delayed_post_at"=>nil,
|
||||
"id"=>@topic.id,
|
||||
"user_name"=>"User Name",
|
||||
"last_reply_at"=>@topic.last_reply_at.as_json,
|
||||
"permissions"=>{"delete"=>true,
|
||||
"reply"=>true,
|
||||
"read"=>true,
|
||||
"attach"=>true,
|
||||
"create"=>true,
|
||||
"update"=>true},
|
||||
"message"=>"<p>content here</p>",
|
||||
"posted_at"=>@topic.posted_at.as_json,
|
||||
"root_topic_id"=>nil,
|
||||
"attachments"=>[{"content-type"=>"unknown/unknown",
|
||||
"url"=>"http://www.example.com/courses/#{@course.id}/files/#{att.id}/download",
|
||||
"filename"=>"content.txt",
|
||||
"display_name"=>"content.txt"}],
|
||||
"topic_children"=>[sub.id]}
|
||||
end
|
||||
|
||||
it "should paginate and return proper pagination headers for courses" do
|
||||
7.times { |i| @course.discussion_topics.create!(:title => i.to_s, :message => i.to_s) }
|
||||
@course.discussion_topics.count.should == 7
|
||||
json = api_call(:get, "/api/v1/courses/#{@course.id}/discussion_topics.json?per_page=3",
|
||||
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :course_id => @course.id.to_s, :per_page => '3'})
|
||||
|
||||
json.length.should == 3
|
||||
response.headers['Link'].should eql(%{</api/v1/courses/#{@course.id}/discussion_topics.json?page=2&per_page=3>; rel="next",</api/v1/courses/#{@course.id}/discussion_topics.json?page=1&per_page=3>; rel="first",</api/v1/courses/#{@course.id}/discussion_topics.json?page=3&per_page=3>; rel="last"})
|
||||
|
||||
# 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 eql(%{</api/v1/courses/#{@course.id}/discussion_topics.json?page=2&per_page=3>; rel="prev",</api/v1/courses/#{@course.id}/discussion_topics.json?page=1&per_page=3>; rel="first",</api/v1/courses/#{@course.id}/discussion_topics.json?page=3&per_page=3>; rel="last"})
|
||||
end
|
||||
|
||||
it "should work with groups" do
|
||||
group = Group.create!(:name=>"group1", :category=>"watup", :context => @course)
|
||||
gtopic = group.discussion_topics.create!(:title => "Group Topic 1", :message => "<p>content here</p>")
|
||||
|
||||
att = Attachment.create!(:filename => 'content.txt', :display_name => "content.txt", :uploaded_data => StringIO.new('attachment content'), :folder => Folder.unfiled_folder(group), :context => group)
|
||||
gtopic.attachment = att
|
||||
gtopic.save
|
||||
|
||||
json = api_call(:get, "/api/v1/groups/#{group.id}/discussion_topics.json",
|
||||
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :group_id => group.id.to_s})
|
||||
json.first.should == {"podcast_url"=>nil,
|
||||
"require_initial_post"=>nil,
|
||||
"title"=>"Group Topic 1",
|
||||
"discussion_subentry_count"=>0,
|
||||
"assignment_id"=>nil,
|
||||
"delayed_post_at"=>nil,
|
||||
"id"=>gtopic.id,
|
||||
"user_name"=>"User Name",
|
||||
"last_reply_at"=>gtopic.last_reply_at.as_json,
|
||||
"permissions"=>
|
||||
{"delete"=>true,
|
||||
"reply"=>true,
|
||||
"read"=>true,
|
||||
"attach"=>true,
|
||||
"create"=>true,
|
||||
"update"=>true},
|
||||
"message"=>"<p>content here</p>",
|
||||
"attachments"=>
|
||||
[{"content-type"=>"unknown/unknown",
|
||||
"url"=>"http://www.example.com/groups/#{group.id}/files/#{att.id}/download",
|
||||
"filename"=>"content.txt",
|
||||
"display_name"=>"content.txt"}],
|
||||
"posted_at"=>gtopic.posted_at.as_json,
|
||||
"root_topic_id"=>nil,
|
||||
"topic_children"=>[]}
|
||||
end
|
||||
|
||||
it "should paginate and return proper pagination headers for groups" do
|
||||
group = Group.create!(:name=>"group1", :category=>"watup", :context => @course)
|
||||
7.times { |i| group.discussion_topics.create!(:title => i.to_s, :message => i.to_s) }
|
||||
group.discussion_topics.count.should == 7
|
||||
json = api_call(:get, "/api/v1/groups/#{group.id}/discussion_topics.json?per_page=3",
|
||||
{:controller => 'discussion_topics', :action => 'index', :format => 'json', :group_id => group.id.to_s, :per_page => '3'})
|
||||
|
||||
json.length.should == 3
|
||||
response.headers['Link'].should eql(%{</api/v1/groups/#{group.id}/discussion_topics.json?page=2&per_page=3>; rel="next",</api/v1/groups/#{group.id}/discussion_topics.json?page=1&per_page=3>; rel="first",</api/v1/groups/#{group.id}/discussion_topics.json?page=3&per_page=3>; rel="last"})
|
||||
|
||||
# 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 eql(%{</api/v1/groups/#{group.id}/discussion_topics.json?page=2&per_page=3>; rel="prev",</api/v1/groups/#{group.id}/discussion_topics.json?page=1&per_page=3>; rel="first",</api/v1/groups/#{group.id}/discussion_topics.json?page=3&per_page=3>; rel="last"})
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue