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:
Bracken Mosbacker 2011-07-21 10:35:46 -06:00
parent d97df4ef7e
commit b3167f4640
7 changed files with 328 additions and 22 deletions

View File

@ -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

View File

@ -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)
{

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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