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:
parent
82abd4754d
commit
d511e04fee
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
28
lib/api.rb
28
lib/api.rb
|
@ -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|
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>")
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue