modules api, closes #10404

also modifies the discussion topic and assignment API
controllers to make sure "must_view" requirements are
fulfilled

test plan:
 * check the API documentation; ensure it looks okay
 * create a course with module items of each supported type
 * set completion criteria of each supported type
 * create another module, so you can set prerequisites
 * use the list modules API and verify its output matches
   the course and the documentation
   * as a teacher, "state" should be missing
   * as a student, "state" should be "locked", "unlocked",
     "started", or "completed"
 * use the show module API and verify the correct information
   is returned for a single module
 * use the list module items API and verify the output
   * as a teacher, the "completion_requirement" omits the
     "completed" flag
   * as a student, "completed" should be true or false,
     depending on whether the requirement was met
 * use the show module API and verify the correct information
   is returned for a single module item
 * last but not least, verify "must view" requirements can
   be fulfilled through the api_data_endpoints supplied
   for files, pages, discussions, and assignments
   * files are viewed when downloading their content
   * pages are viewed by the show action (where content
     is returned)
   * discussions are viewed when marked read via the
     mark_topic_read or mark_all_read actions
   * assignments are viewed by the show action
     (where description is returned).  they are not viewed
     if the assignment is locked and the user does not
     have access to the content yet.

Change-Id: I0cbbbc542f69215e7b396a501d4d86ff2f76c149
Reviewed-on: https://gerrit.instructure.com/13626
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
This commit is contained in:
Jeremy Stanley 2012-09-11 11:16:48 -06:00
parent 82abd4754d
commit d511e04fee
12 changed files with 695 additions and 19 deletions

View File

@ -164,7 +164,8 @@ class AssignmentsApiController < ApplicationController
@assignment = @context.active_assignments.find(params[:id],
:include => [:assignment_group, :rubric_association, :rubric])
render :json => assignment_json(@assignment, @current_user, session).to_json
@assignment.context_module_action(@current_user, :read) unless @assignment.locked_for?(@current_user, :check_policies => true)
render :json => assignment_json(@assignment, @current_user, session)
end
end

View File

@ -0,0 +1,193 @@
#
# Copyright (C) 2012 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/>.
#
# @API Modules
#
# Modules are collections of learning materials useful for organizing courses
# and optionally providing a linear flow through them. Module items can be
# accessed linearly or sequentially depending on module configuration. Items
# can be unlocked by various criteria such as reading a page or achieving a
# minimum score on a quiz. Modules themselves can be unlocked by the completion
# of other Modules.
#
# @object Module
# {
# // the unique identifier for the module
# id: 123,
#
# // the 0-based position of this module (0 == first)
# position: 2,
#
# // the name of this module
# name: "Imaginary Numbers and You",
#
# // (Optional) the date this module will unlock
# unlock_at: "2012-12-31T06:00:00-06:00",
#
# // Whether module items must be unlocked in order
# require_sequential_progress: true,
#
# // IDs of Modules that must be completed before this one is unlocked
# prerequisite_module_ids: [121, 122],
#
# // The state of this Module for the calling user
# // one of 'locked', 'unlocked', 'started', 'completed'
# // (Optional; present only if the caller is a student)
# state: 'started',
#
# // the date the calling user completed the module
# // (Optional; present only if the caller is a student)
# completed_at: nil
# }
#
# @object Module Item
# {
# // the unique identifier for the module item
# id: 768,
#
# // the 0-based position of this item in the module
# position: 0,
#
# // the title of this item
# title: "Square Roots: Irrational numbers or boxy vegetables?",
#
# // 0-based indent level; module items may be indented to show a hierarchy
# indent: 0,
#
# // the type of object referred to
# // one of "File", "Page", "Discussion", "Assignment", "Quiz", "SubHeader",
# // "ExternalUrl", "ExternalTool"
# type: "Assignment",
#
# // link to the item in Canvas
# url: "https://canvas.example.edu/courses/222/modules/items/768",
#
# // (Optional) link to the Canvas API object, if applicable
# data_api_endpoint: "https://canvas.example.edu/api/v1/courses/222/assignments/987",
#
# // Completion requirement for this module item
# // (Optional; present only if the caller is a student)
# completion_requirement: {
# // one of "must_view", "must_submit", "must_contribute", "min_score"
# type: "min_score",
#
# // minimum score required to complete (only present when type == 'min_score')
# min_score: 10,
#
# // whether the calling user has met this requirement
# completed: true
# }
# }
class ContextModulesApiController < ApplicationController
before_filter :require_context
include Api::V1::ContextModule
# @API List modules
#
# List the modules in a course
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/222/modules
#
# @returns [Module]
def index
if authorized_action(@context, @current_user, :read)
route = polymorphic_url([:api_v1, @context, :context_modules])
scope = @context.context_modules.active
modules = Api.paginate(scope, self, route)
modules_and_progressions = if @context.grants_right?(@current_user, session, :participate_as_student)
modules.map { |m| [m, m.evaluate_for(@current_user)] }
else
modules.map { |m| [m, nil] }
end
render :json => modules_and_progressions.map { |mod, prog| module_json(mod, @current_user, session, prog) }
end
end
# @API Show module
#
# Get information about a single module
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/222/modules/123
#
# @returns Module
def show
if authorized_action(@context, @current_user, :read)
mod = @context.context_modules.active.find(params[:id])
prog = @context.grants_right?(@current_user, session, :participate_as_student) ? mod.evaluate_for(@current_user) : nil
render :json => module_json(mod, @current_user, session, prog)
end
end
# @API List module items
#
# List the items in a module
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/222/modules/123/items
#
# @returns [Module Item]
def list_module_items
if authorized_action(@context, @current_user, :read)
mod = @context.context_modules.active.find(params[:module_id])
route = polymorphic_url([:api_v1, @context, mod, :items])
scope = mod.content_tags.active
items = Api.paginate(scope, self, route)
prog = @context.grants_right?(@current_user, session, :participate_as_student) ? mod.evaluate_for(@current_user) : nil
render :json => items.map { |item| module_item_json(item, @current_user, session, mod, prog) }
end
end
# @API Show module item
#
# Get information about a single module item
#
# @example_request
# curl -H 'Authorization: Bearer <token>' \
# https://<canvas>/api/v1/courses/222/modules/123/items/768
#
# @returns Module Item
def show_module_item
if authorized_action(@context, @current_user, :read)
mod = @context.context_modules.active.find(params[:module_id])
item = mod.content_tags.active.find(params[:id])
prog = @context.grants_right?(@current_user, session, :participate_as_student) ? mod.evaluate_for(@current_user) : nil
render :json => module_item_json(item, @current_user, session, mod, prog)
end
end
# Mark an external URL content tag read for purposes of module progression,
# then redirect to the URL (vs. render in an iframe like content_tag_redirect).
# Not documented directly; part of an opaque URL returned by above endpoints.
def module_item_redirect
if authorized_action(@context, @current_user, :read)
@tag = @context.context_module_tags.active.find(params[:id])
if @tag.content_type == 'ExternalUrl'
@tag.context_module_action(@current_user, :read)
redirect_to @tag.url
else
return render(:status => 400, :json => { :message => "incorrect module item type" })
end
end
end
end

View File

@ -19,8 +19,8 @@
class ContextModulesController < ApplicationController
before_filter :require_context
add_crumb(proc { t('#crumbs.modules', "Modules") }) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_context_modules_url }
before_filter { |c| c.active_tab = "modules" }
before_filter { |c| c.active_tab = "modules" }
def index
if authorized_action(@context, @current_user, :read)
@modules = @context.context_modules.active

View File

@ -187,7 +187,6 @@ class DiscussionTopicsController < ApplicationController
@headers = !params[:headless]
@locked = @topic.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
unless @locked
@topic.context_module_action(@current_user, :read)
@topic.change_read_state('read', @current_user)
end
if @topic.for_group_assignment?

View File

@ -85,7 +85,7 @@ class WikiPagesController < ApplicationController
# @returns [Page]
def api_index
if authorized_action(@context.wiki.wiki_pages.new, @current_user, :read)
pages_route = polymorphic_url([:api_v1, @context, :pages])
pages_route = polymorphic_url([:api_v1, @context, :wiki_pages])
scope = @context.wiki.wiki_pages.active.order_by_id
view_hidden = is_authorized_action?(@context.wiki.wiki_pages.new(:hide_from_students => true), @current_user, :read)
scope = scope.visible_to_students unless view_hidden

View File

@ -239,6 +239,7 @@ class DiscussionTopic < ActiveRecord::Base
return nil unless current_user
if new_state != self.read_state(current_user)
self.context_module_action(current_user, :read) if new_state == 'read'
self.update_or_create_participant(:current_user => current_user, :new_state => new_state)
else
true
@ -250,6 +251,8 @@ class DiscussionTopic < ActiveRecord::Base
return unless current_user
transaction do
self.context_module_action(current_user, :read) if new_state == 'read'
new_count = (new_state == 'unread' ? self.default_unread_count : 0)
self.update_or_create_participant(:current_user => current_user, :new_state => new_state, :new_count => new_count)

View File

@ -919,7 +919,8 @@ ActionController::Routing::Routes.draw do |map|
api.with_options(:controller => :files) do |files|
files.post 'files/:id/create_success', :action => :api_create_success, :path_name => 'files_create_success'
files.get 'files/:id/create_success', :action => :api_create_success, :path_name => 'files_create_success'
files.get 'files/:id', :action => :api_show, :path_name => 'file'
# 'attachment' (rather than 'file') is used below so modules API can use polymorphic_url to generate an item API link
files.get 'files/:id', :action => :api_show, :path_name => 'attachment'
files.delete 'files/:id', :action => :destroy
files.put 'files/:id', :action => :api_update
files.get 'files/:id/:uuid/status', :action => :api_file_status, :path_name => 'file_status'
@ -943,10 +944,18 @@ ActionController::Routing::Routes.draw do |map|
end
api.with_options(:controller => :wiki_pages) do |wiki_pages|
wiki_pages.get "courses/:course_id/pages", :action => :api_index, :path_name => 'course_pages'
wiki_pages.get "groups/:group_id/pages", :action => :api_index, :path_name => 'group_pages'
wiki_pages.get "courses/:course_id/pages/:url", :action => :api_show, :path_name => 'course_page'
wiki_pages.get "groups/:group_id/pages/:url", :action => :api_show, :path_name => 'group_page'
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'
end
api.with_options(:controller => :context_modules_api) do |context_modules|
context_modules.get "courses/:course_id/modules", :action => :index, :path_name => 'course_context_modules'
context_modules.get "courses/:course_id/modules/:id", :action => :show, :path_name => 'course_context_module'
context_modules.get "courses/:course_id/modules/:module_id/items", :action => :list_module_items, :path_name => 'course_context_module_items'
context_modules.get "courses/:course_id/modules/:module_id/items/:id", :action => :show_module_item, :path_name => 'course_context_module_item'
context_modules.get "courses/:course_id/module_item_redirect/:id", :action => :module_item_redirect, :path_name => 'course_context_module_item_redirect'
end
end

View File

@ -301,6 +301,16 @@ module Api
# regex for shard-aware ID
ID = '(?:\d+~)?\d+'
# maps a Canvas data type to an API-friendly type name
API_DATA_TYPE = { "Attachment" => "File",
"WikiPage" => "Page",
"DiscussionTopic" => "Discussion",
"Assignment" => "Assignment",
"Quiz" => "Quiz",
"ContextModuleSubHeader" => "SubHeader",
"ExternalUrl" => "ExternalUrl",
"ContextExternalTool" => "ExternalTool" }.freeze
# maps canvas URLs to API URL helpers
# target array is return type, helper, name of each capture, and optionally a Hash of extra arguments
API_ROUTE_MAP = {
@ -313,12 +323,12 @@ module Api
%r{^/groups/(#{ID})/discussion_topics/(#{ID})$} => ['Discussion', :api_v1_group_discussion_topic_url, :group_id, :topic_id],
# List pages
%r{^/courses/(#{ID})/wiki$} => ['[Page]', :api_v1_course_pages_url, :course_id],
%r{^/groups/(#{ID})/wiki$} => ['[Page]', :api_v1_group_pages_url, :group_id],
%r{^/courses/(#{ID})/wiki$} => ['[Page]', :api_v1_course_wiki_pages_url, :course_id],
%r{^/groups/(#{ID})/wiki$} => ['[Page]', :api_v1_group_wiki_pages_url, :group_id],
# Show page
%r{^/courses/(#{ID})/wiki/([^/]+)$} => ['Page', :api_v1_course_page_url, :course_id, :url],
%r{^/groups/(#{ID})/wiki/([^/]+)$} => ['Page', :api_v1_group_page_url, :group_id, :url],
%r{^/courses/(#{ID})/wiki/([^/]+)$} => ['Page', :api_v1_course_wiki_page_url, :course_id, :url],
%r{^/groups/(#{ID})/wiki/([^/]+)$} => ['Page', :api_v1_group_wiki_page_url, :group_id, :url],
# List assignments
%r{^/courses/(#{ID})/assignments$} => ['[Assignment]', :api_v1_course_assignments_url, :course_id],
@ -332,11 +342,11 @@ module Api
%r{^/users/(#{ID})/files$} => ['Folder', :api_v1_user_folder_url, :user_id, {:id => 'root'}],
# Get file
%r{^/courses/#{ID}/files/(#{ID})/} => ['File', :api_v1_file_url, :id],
%r{^/groups/#{ID}/files/(#{ID})/} => ['File', :api_v1_file_url, :id],
%r{^/users/#{ID}/files/(#{ID})/} => ['File', :api_v1_file_url, :id],
%r{^/files/(#{ID})/} => ['File', :api_v1_file_url, :id],
}
%r{^/courses/#{ID}/files/(#{ID})/} => ['File', :api_v1_attachment_url, :id],
%r{^/groups/#{ID}/files/(#{ID})/} => ['File', :api_v1_attachment_url, :id],
%r{^/users/#{ID}/files/(#{ID})/} => ['File', :api_v1_attachment_url, :id],
%r{^/files/(#{ID})/} => ['File', :api_v1_attachment_url, :id],
}.freeze
def api_endpoint_info(protocol, host, url)
API_ROUTE_MAP.each_pair do |re, api_route|

View File

@ -0,0 +1,80 @@
#
# Copyright (C) 2012 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::ContextModule
include Api::V1::Json
include Api::V1::User
MODULE_JSON_ATTRS = %w(id position name unlock_at)
MODULE_ITEM_JSON_ATTRS = %w(id position title indent)
# optionally pass progression to include 'state', 'completed_at'
def module_json(context_module, current_user, session, progression = nil)
hash = api_json(context_module, current_user, session, :only => MODULE_JSON_ATTRS)
hash['require_sequential_progress'] = !!context_module.require_sequential_progress
hash['prerequisite_module_ids'] = context_module.prerequisites.reject{|p| p[:type] != 'context_module'}.map{|p| p[:id]}
if progression
hash['state'] = progression.workflow_state
hash['completed_at'] = progression.completed_at
end
hash
end
# optionally pass context_module to avoid redundant queries when rendering multiple items
# optionally pass progression to include completion status
def module_item_json(content_tag, current_user, session, context_module = nil, progression = nil)
context_module ||= content_tag.context_module
hash = api_json(content_tag, current_user, session, :only => MODULE_ITEM_JSON_ATTRS)
hash['type'] = Api::API_DATA_TYPE[content_tag.content_type] || content_tag.content_type
# add canvas web url
unless content_tag.content_type == 'ContextModuleSubHeader'
hash['url'] = case content_tag.content_type
when 'ExternalUrl'
# API prefers to redirect to the external page, rather than host in an iframe
api_v1_course_context_module_item_redirect_url(:id => content_tag.id)
else
# otherwise we'll link to the same thing the web UI does
course_context_modules_item_redirect_url(:id => content_tag.id)
end
end
# add data-api-endpoint link, if applicable
api_url = nil
case content_tag.content_type
# course context
when 'Assignment', 'WikiPage', 'DiscussionTopic'
api_url = polymorphic_url([:api_v1, context_module.context, content_tag.content])
# no context
when 'Attachment'
api_url = polymorphic_url([:api_v1, content_tag.content])
end
hash['data_api_endpoint'] = api_url if api_url
# add completion requirements
if criterion = context_module.completion_requirements && context_module.completion_requirements.detect { |r| r[:id] == content_tag.id }
ch = { 'type' => criterion[:type] }
ch['min_score'] = criterion[:min_score] if criterion[:type] == 'min_score'
ch['completed'] = !!progression.requirements_met.detect{|r|r[:type] == criterion[:type] && r[:id] == content_tag.id} if progression
hash['completion_requirement'] = ch
end
hash
end
end

View File

@ -338,4 +338,49 @@ describe AssignmentsApiController, :type => :integration do
json['description']
end
end
it "should fulfill module progression requirements" do
course_with_student(:active_all => true)
@assignment = @course.assignments.create! :title => "Test Assignment", :description => "public stuff"
mod = @course.context_modules.create!(:name => "some module")
tag = mod.add_item(:id => @assignment.id, :type => 'assignment')
mod.completion_requirements = { tag.id => {:type => 'must_view'} }
mod.save!
# index should not affect anything
api_call(:get,
"/api/v1/courses/#{@course.id}/assignments.json",
{ :controller => 'assignments_api', :action => 'index',
:format => 'json', :course_id => @course.id.to_s })
mod.evaluate_for(@user).should be_unlocked
# show should count as a view
json = api_call(:get,
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}.json",
{ :controller => "assignments_api", :action => "show",
:format => "json", :course_id => @course.id.to_s,
:id => @assignment.id.to_s })
json['description'].should_not be_nil
mod.evaluate_for(@user).should be_completed
end
it "should not fulfill requirements when description isn't returned" do
course_with_student(:active_all => true)
@assignment = @course.assignments.create! :title => "Locked Assignment", :description => "locked!"
@assignment.any_instantiation.expects(:locked_for?).returns({:asset_string => '', :unlock_at => 1.hour.from_now}).at_least(1)
mod = @course.context_modules.create!(:name => "some module")
tag = mod.add_item(:id => @assignment.id, :type => 'assignment')
mod.completion_requirements = { tag.id => {:type => 'must_view'} }
mod.save!
json = api_call(:get,
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}.json",
{ :controller => "assignments_api", :action => "show",
:format => "json", :course_id => @course.id.to_s,
:id => @assignment.id.to_s })
json['description'].should be_nil
mod.evaluate_for(@user).should be_unlocked
end
end

View File

@ -481,6 +481,36 @@ describe DiscussionTopicsController, :type => :integration do
].join(',')
end
it "should fulfill module viewed requirements when marking a topic read" do
@module = @course.context_modules.create!(:name => "some module")
@topic = create_topic(@course, :title => "Topic 1", :message => "<p>content here</p>")
tag = @module.add_item(:id => @topic.id, :type => 'discussion_topic')
@module.completion_requirements = { tag.id => {:type => 'must_view'} }
@module.save!
course_with_student(:course => @course)
@module.evaluate_for(@user).should be_unlocked
raw_api_call(:put, "/api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/read",
{ :controller => 'discussion_topics_api', :action => 'mark_topic_read', :format => 'json',
:course_id => @course.id.to_s, :topic_id => @topic.id.to_s })
@module.evaluate_for(@user).should be_completed
end
it "should fulfill module viewed requirements when marking a topic and all its entries read" do
@module = @course.context_modules.create!(:name => "some module")
@topic = create_topic(@course, :title => "Topic 1", :message => "<p>content here</p>")
tag = @module.add_item(:id => @topic.id, :type => 'discussion_topic')
@module.completion_requirements = { tag.id => {:type => 'must_view'} }
@module.save!
course_with_student(:course => @course)
@module.evaluate_for(@user).should be_unlocked
raw_api_call(:put, "/api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}/read_all",
{ :controller => 'discussion_topics_api', :action => 'mark_all_read', :format => 'json',
:course_id => @course.id.to_s, :topic_id => @topic.id.to_s })
@module.evaluate_for(@user).should be_completed
end
context "creating an entry under a topic" do
before :each do
@topic = create_topic(@course, :title => "Topic 1", :message => "<p>content here</p>")

View File

@ -0,0 +1,306 @@
#
# Copyright (C) 2012 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 "Modules API", :type => :integration do
before do
course.offer!
@module1 = @course.context_modules.create!(:name => "module1")
@assignment = @course.assignments.create!(:name => "pls submit", :submission_types => ["online_text_entry"])
@assignment_tag = @module1.add_item(:id => @assignment.id, :type => 'assignment')
@quiz = @course.quizzes.create!(:title => "score 10")
@quiz_tag = @module1.add_item(:id => @quiz.id, :type => 'quiz')
@topic = @course.discussion_topics.create!(:message => 'pls contribute')
@topic_tag = @module1.add_item(:id => @topic.id, :type => 'discussion_topic')
@subheader_tag = @module1.add_item(:type => 'context_module_sub_header', :title => 'external resources')
@external_url_tag = @module1.add_item(:type => 'external_url', :url => 'http://example.com/lolcats',
:title => 'pls view', :indent => 1)
@module1.completion_requirements = {
@assignment_tag.id => { :type => 'must_submit' },
@quiz_tag.id => { :type => 'min_score', :min_score => 10 },
@topic_tag.id => { :type => 'must_contribute' },
@external_url_tag.id => { :type => 'must_view' } }
@module1.save!
@christmas = Time.zone.local(Time.now.year + 1, 12, 25, 7, 0)
@module2 = @course.context_modules.create!(:name => "do not open until christmas",
:unlock_at => @christmas,
:require_sequential_progress => true)
@module2.prerequisites = "module_#{@module1.id}"
@wiki_page = @course.wiki.wiki_page
@wiki_page.workflow_state = 'active'; @wiki_page.save!
@wiki_page_tag = @module2.add_item(:id => @wiki_page.id, :type => 'wiki_page')
@attachment = attachment_model(:context => @course)
@attachment_tag = @module2.add_item(:id => @attachment.id, :type => 'attachment')
@module2.save!
end
context "as a teacher" do
before :each do
course_with_teacher(:course => @course, :active_all => true)
end
it "should list modules" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules",
:controller => "context_modules_api", :action => "index", :format => "json",
:course_id => "#{@course.id}")
json.should == [
{
"name" => @module1.name,
"unlock_at" => nil,
"position" => 1,
"require_sequential_progress" => false,
"prerequisite_module_ids" => [],
"id" => @module1.id
},
{
"name" => @module2.name,
"unlock_at" => @christmas.as_json,
"position" => 2,
"require_sequential_progress" => true,
"prerequisite_module_ids" => [@module1.id],
"id" => @module2.id
}
]
end
it "should show a single module" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module2.id}",
:controller => "context_modules_api", :action => "show", :format => "json",
:course_id => "#{@course.id}", :id => "#{@module2.id}")
json.should == {
"name" => @module2.name,
"unlock_at" => @christmas.as_json,
"position" => 2,
"require_sequential_progress" => true,
"prerequisite_module_ids" => [@module1.id],
"id" => @module2.id
}
end
it "should list module items" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items",
:controller => "context_modules_api", :action => "list_module_items", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}")
json.should == [
{
"type" => "Assignment",
"id" => @assignment_tag.id,
"url" => "http://www.example.com/courses/#{@course.id}/modules/items/#{@assignment_tag.id}",
"position" => 1,
"data_api_endpoint" => "http://www.example.com/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}",
"title" => @assignment_tag.title,
"indent" => 0,
"completion_requirement" => { "type" => "must_submit" }
},
{
"type" => "Quiz",
"id" => @quiz_tag.id,
"url" => "http://www.example.com/courses/#{@course.id}/modules/items/#{@quiz_tag.id}",
"position" => 2,
"title" => @quiz_tag.title,
"indent" => 0,
"completion_requirement" => { "type" => "min_score", "min_score" => 10 }
},
{
"type" => "Discussion",
"id" => @topic_tag.id,
"url" => "http://www.example.com/courses/#{@course.id}/modules/items/#{@topic_tag.id}",
"position" => 3,
"data_api_endpoint" => "http://www.example.com/api/v1/courses/#{@course.id}/discussion_topics/#{@topic.id}",
"title" => @topic_tag.title,
"indent" => 0,
"completion_requirement" => { "type" => "must_contribute" }
},
{
"type" => "SubHeader",
"id" => @subheader_tag.id,
"position" => 4,
"title" => @subheader_tag.title,
"indent" => 0
},
{
"type" => "ExternalUrl",
"id" => @external_url_tag.id,
"url" => "http://www.example.com/api/v1/courses/#{@course.id}/module_item_redirect/#{@external_url_tag.id}",
"position" => 5,
"title" => @external_url_tag.title,
"indent" => 1,
"completion_requirement" => { "type" => "must_view" }
}
]
end
it "should show module items individually" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module2.id}/items/#{@wiki_page_tag.id}",
:controller => "context_modules_api", :action => "show_module_item", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module2.id}",
:id => "#{@wiki_page_tag.id}")
json.should == {
"type" => "Page",
"id" => @wiki_page_tag.id,
"url" => "http://www.example.com/courses/#{@course.id}/modules/items/#{@wiki_page_tag.id}",
"position" => 1,
"title" => @wiki_page_tag.title,
"indent" => 0,
"data_api_endpoint" => "http://www.example.com/api/v1/courses/#{@course.id}/pages/#{@wiki_page.url}"
}
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module2.id}/items/#{@attachment_tag.id}",
:controller => "context_modules_api", :action => "show_module_item", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module2.id}",
:id => "#{@attachment_tag.id}")
json.should == {
"type" => "File",
"id" => @attachment_tag.id,
"url" => "http://www.example.com/courses/#{@course.id}/modules/items/#{@attachment_tag.id}",
"position" => 2,
"title" => @attachment_tag.title,
"indent" => 0,
"data_api_endpoint" => "http://www.example.com/api/v1/files/#{@attachment.id}"
}
end
it "should paginate the module list" do
# 2 modules already exist
9.times { |i| @course.context_modules.create!(:name => "spurious module #{i}") }
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules",
:controller => "context_modules_api", :action => "index", :format => "json",
:course_id => "#{@course.id}")
response.headers["Link"].should be_present
json.size.should == 10
ids = json.collect{ |mod| mod['id'] }
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules?page=2",
:controller => "context_modules_api", :action => "index", :format => "json",
:course_id => "#{@course.id}", :page => "2")
json.size.should == 1
ids += json.collect{ |mod| mod['id'] }
ids.should == @course.context_modules.sort_by(&:position).collect(&:id)
end
it "should paginate the module item list" do
module3 = @course.context_modules.create!(:name => "module with lots of items")
11.times { |i| module3.add_item(:type => 'context_module_sub_header', :title => "item #{i}") }
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{module3.id}/items",
:controller => "context_modules_api", :action => "list_module_items", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{module3.id}")
response.headers["Link"].should be_present
json.size.should == 10
ids = json.collect{ |tag| tag['id'] }
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{module3.id}/items?page=2",
:controller => "context_modules_api", :action => "list_module_items", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{module3.id}", :page => "2")
json.size.should == 1
ids += json.collect{ |tag| tag['id'] }
ids.should == module3.content_tags.sort_by(&:position).collect(&:id)
end
end
context "as a student" do
before :each do
course_with_student(:course => @course, :active_all => true)
end
it "should show locked state" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module2.id}",
:controller => "context_modules_api", :action => "show", :format => "json",
:course_id => "#{@course.id}", :id => "#{@module2.id}")
json['state'].should == 'locked'
end
it "should show module progress" do
# to simplify things, eliminate the requirements on the quiz and discussion topic for this test
@module1.completion_requirements.reject! {|r| [@quiz_tag.id, @topic_tag.id].include? r[:id]}
@module1.save!
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}",
:controller => "context_modules_api", :action => "show", :format => "json",
:course_id => "#{@course.id}", :id => "#{@module1.id}")
json['state'].should == 'unlocked'
@assignment.submit_homework(@user, :body => "done!")
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}",
:controller => "context_modules_api", :action => "show", :format => "json",
:course_id => "#{@course.id}", :id => "#{@module1.id}")
json['state'].should == 'started'
json['completed_at'].should be_nil
@external_url_tag.context_module_action(@user, :read)
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}",
:controller => "context_modules_api", :action => "show", :format => "json",
:course_id => "#{@course.id}", :id => "#{@module1.id}")
json['state'].should == 'completed'
json['completed_at'].should_not be_nil
end
it "should show module item completion" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items/#{@assignment_tag.id}",
:controller => "context_modules_api", :action => "show_module_item", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}",
:id => "#{@assignment_tag.id}")
json['completion_requirement']['type'].should == 'must_submit'
json['completion_requirement']['completed'].should be_false
@assignment.submit_homework(@user, :body => "done!")
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items/#{@assignment_tag.id}",
:controller => "context_modules_api", :action => "show_module_item", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}",
:id => "#{@assignment_tag.id}")
json['completion_requirement']['completed'].should be_true
end
it "should mark viewed and redirect external URLs" do
raw_api_call(:get, "/api/v1/courses/#{@course.id}/module_item_redirect/#{@external_url_tag.id}",
:controller => "context_modules_api", :action => "module_item_redirect",
:format => "json", :course_id => "#{@course.id}", :id => "#{@external_url_tag.id}")
response.should redirect_to "http://example.com/lolcats"
@module1.evaluate_for(@user).requirements_met.should be_any {
|rm| rm[:type] == "must_view" && rm[:id] == @external_url_tag.id }
end
it "should check permissions" do
user
api_call(:get, "/api/v1/courses/#{@course.id}/modules",
{ :controller => "context_modules_api", :action => "index", :format => "json",
:course_id => "#{@course.id}"}, {}, {}, {:expected_status => 401})
api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module2.id}",
{ :controller => "context_modules_api", :action => "show", :format => "json",
:course_id => "#{@course.id}", :id => "#{@module2.id}"},
{}, {}, {:expected_status => 401})
api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items",
{ :controller => "context_modules_api", :action => "list_module_items", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}"},
{}, {}, { :expected_status => 401 })
api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module2.id}/items/#{@attachment_tag.id}",
{ :controller => "context_modules_api", :action => "show_module_item", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module2.id}",
:id => "#{@attachment_tag.id}"}, {}, {}, { :expected_status => 401 })
api_call(:get, "/api/v1/courses/#{@course.id}/module_item_redirect/#{@external_url_tag.id}",
{ :controller => "context_modules_api", :action => "module_item_redirect",
:format => "json", :course_id => "#{@course.id}", :id => "#{@external_url_tag.id}"},
{}, {}, { :expected_status => 401 })
end
end
end