2013-04-30 05:24:20 +08:00
|
|
|
#
|
|
|
|
# Copyright (C) 2013 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
|
|
|
|
# @subtopic Module Items
|
|
|
|
#
|
|
|
|
# @object Module Item
|
|
|
|
# {
|
|
|
|
# // the unique identifier for the module item
|
|
|
|
# id: 768,
|
|
|
|
#
|
|
|
|
# // the position of this item in the module (1-based)
|
|
|
|
# position: 1,
|
|
|
|
#
|
|
|
|
# // 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",
|
|
|
|
#
|
2013-05-14 05:54:11 +08:00
|
|
|
# // the id of the object referred to
|
|
|
|
# // applies to "File", "Discussion", "Assignment", "Quiz", "ExternalTool" types
|
2013-05-10 01:50:11 +08:00
|
|
|
# content_id: 1337,
|
|
|
|
#
|
2013-04-30 05:24:20 +08:00
|
|
|
# // link to the item in Canvas
|
|
|
|
# html_url: "https://canvas.example.edu/courses/222/modules/items/768",
|
|
|
|
#
|
|
|
|
# // (Optional) link to the Canvas API object, if applicable
|
|
|
|
# url: "https://canvas.example.edu/api/v1/courses/222/assignments/987",
|
|
|
|
#
|
2013-05-14 05:54:11 +08:00
|
|
|
# // (only for 'Page' type) unique locator for the linked wiki page
|
|
|
|
# page_url: "my-page-title"
|
|
|
|
#
|
2013-04-30 05:24:20 +08:00
|
|
|
# // (only for 'ExternalUrl' and 'ExternalTool' types) external url that the item points to
|
|
|
|
# external_url: "https://www.example.com/externalurl",
|
|
|
|
#
|
|
|
|
# // (only for 'ExternalTool' type) whether the external tool opens in a new tab
|
|
|
|
# new_tab: false,
|
|
|
|
#
|
|
|
|
# // Completion requirement for this module item
|
|
|
|
# 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
|
|
|
|
# // (Optional; present only if the caller is a student)
|
|
|
|
# completed: true
|
2013-07-28 07:40:53 +08:00
|
|
|
# },
|
|
|
|
#
|
|
|
|
# // (Present only if requested through include[]=content_details)
|
|
|
|
# // If applicable, returns additional details specific to the associated object
|
|
|
|
# content_details: {
|
|
|
|
# points_possible: 20,
|
|
|
|
# due_at: "2012-12-31T06:00:00-06:00",
|
|
|
|
# unlock_at: "2012-12-31T06:00:00-06:00",
|
|
|
|
# lock_at: "2012-12-31T06:00:00-06:00"
|
2013-04-30 05:24:20 +08:00
|
|
|
# }
|
2013-07-28 07:40:53 +08:00
|
|
|
#
|
2013-04-30 05:24:20 +08:00
|
|
|
# }
|
|
|
|
class ContextModuleItemsApiController < ApplicationController
|
|
|
|
before_filter :require_context
|
|
|
|
include Api::V1::ContextModule
|
|
|
|
|
|
|
|
# @API List module items
|
|
|
|
#
|
|
|
|
# List the items in a module
|
|
|
|
#
|
2013-08-14 07:00:40 +08:00
|
|
|
# @argument include[] [String, "content_details"]
|
|
|
|
# If included, will return additional details specific to the content
|
|
|
|
# associated with each item. Refer to the {api:Modules:Module%20Item Module
|
|
|
|
# Item specification} for more details.
|
|
|
|
#
|
|
|
|
# @argument search_term [Optional, String]
|
|
|
|
# The partial title of the items to match and return.
|
2013-07-28 07:40:53 +08:00
|
|
|
#
|
2013-04-30 05:24:20 +08:00
|
|
|
# @example_request
|
|
|
|
# curl -H 'Authorization: Bearer <token>' \
|
|
|
|
# https://<canvas>/api/v1/courses/222/modules/123/items
|
|
|
|
#
|
|
|
|
# @returns [Module Item]
|
|
|
|
def index
|
|
|
|
if authorized_action(@context, @current_user, :read)
|
|
|
|
mod = @context.modules_visible_to(@current_user).find(params[:module_id])
|
2013-07-28 07:40:53 +08:00
|
|
|
ContextModule.send(:preload_associations, mod, {:content_tags => :content})
|
2013-04-30 05:24:20 +08:00
|
|
|
route = polymorphic_url([:api_v1, @context, mod, :items])
|
2013-05-15 03:53:14 +08:00
|
|
|
scope = mod.content_tags_visible_to(@current_user)
|
2013-07-30 03:57:27 +08:00
|
|
|
scope = ContentTag.search_by_attribute(scope, :title, params[:search_term])
|
2013-04-30 05:24:20 +08:00
|
|
|
items = Api.paginate(scope, self, route)
|
|
|
|
prog = @context.grants_right?(@current_user, session, :participate_as_student) ? mod.evaluate_for(@current_user) : nil
|
2013-07-28 07:40:53 +08:00
|
|
|
render :json => items.map { |item| module_item_json(item, @current_user, session, mod, prog, Array(params[:include])) }
|
2013-04-30 05:24:20 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @API Show module item
|
|
|
|
#
|
|
|
|
# Get information about a single module item
|
|
|
|
#
|
2013-08-14 07:00:40 +08:00
|
|
|
# @argument include[] [String, "content_details"]
|
|
|
|
# If included, will return additional details specific to the content
|
|
|
|
# associated with this item. Refer to the {api:Modules:Module%20Item Module
|
|
|
|
# Item specification} for more details.
|
2013-07-28 07:40:53 +08:00
|
|
|
#
|
2013-04-30 05:24:20 +08:00
|
|
|
# @example_request
|
|
|
|
# curl -H 'Authorization: Bearer <token>' \
|
|
|
|
# https://<canvas>/api/v1/courses/222/modules/123/items/768
|
|
|
|
#
|
|
|
|
# @returns Module Item
|
|
|
|
def show
|
|
|
|
if authorized_action(@context, @current_user, :read)
|
|
|
|
mod = @context.modules_visible_to(@current_user).find(params[:module_id])
|
2013-05-15 03:53:14 +08:00
|
|
|
item = mod.content_tags_visible_to(@current_user).find(params[:id])
|
2013-04-30 05:24:20 +08:00
|
|
|
prog = @context.grants_right?(@current_user, session, :participate_as_student) ? mod.evaluate_for(@current_user) : nil
|
2013-07-28 07:40:53 +08:00
|
|
|
render :json => module_item_json(item, @current_user, session, mod, prog, Array(params[:include]))
|
2013-04-30 05:24:20 +08:00
|
|
|
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 redirect
|
|
|
|
if authorized_action(@context, @current_user, :read)
|
2013-05-23 00:42:33 +08:00
|
|
|
@tag = @context.context_module_tags.not_deleted.find(params[:id])
|
|
|
|
if !(@tag.unpublished? || @tag.context_module.unpublished?) || authorized_action(@tag.context_module, @current_user, :update)
|
|
|
|
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
|
2013-04-30 05:24:20 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @API Create a module item
|
|
|
|
#
|
|
|
|
# Create and return a new module item
|
|
|
|
#
|
2013-08-14 07:00:40 +08:00
|
|
|
# @argument module_item[title] [Optional, String]
|
|
|
|
# The name of the module item and associated content
|
|
|
|
#
|
|
|
|
# @argument module_item[type] [String, "File"|"Page"|"Discussion"|"Assignment"|"Quiz"|"SubHeader"|"ExternalUrl"|"ExternalTool"]
|
|
|
|
# The type of content linked to the item
|
|
|
|
#
|
|
|
|
# @argument module_item[content_id] [String]
|
|
|
|
# The id of the content to link to the module item. Required, except for
|
|
|
|
# 'ExternalUrl', 'Page', and 'SubHeader' types.
|
|
|
|
#
|
|
|
|
# @argument module_item[position] [Optional, Integer]
|
|
|
|
# The position of this item in the module (1-based).
|
|
|
|
#
|
|
|
|
# @argument module_item[indent] [Optional, Integer]
|
|
|
|
# 0-based indent level; module items may be indented to show a hierarchy
|
|
|
|
#
|
|
|
|
# @argument module_item[page_url] [String]
|
|
|
|
# Suffix for the linked wiki page (e.g. 'front-page'). Required for 'Page'
|
|
|
|
# type.
|
|
|
|
#
|
|
|
|
# @argument module_item[external_url] [String]
|
|
|
|
# External url that the item points to. [Required for 'ExternalUrl' and
|
|
|
|
# 'ExternalTool' types.
|
|
|
|
#
|
|
|
|
# @argument module_item[new_tab] [Optional, Boolean]
|
|
|
|
# Whether the external tool opens in a new tab. Only applies to
|
|
|
|
# 'ExternalTool' type.
|
|
|
|
#
|
|
|
|
# @argument module_item[completion_requirement][type] [Optional, String, "must_view"|"must_contribute"|"must_submit"]
|
|
|
|
# Completion requirement for this module item.
|
2013-04-30 05:24:20 +08:00
|
|
|
# "must_view": Applies to all item types
|
|
|
|
# "must_contribute": Only applies to "Assignment", "Discussion", and "Page" types
|
|
|
|
# "must_submit", "min_score": Only apply to "Assignment" and "Quiz" types
|
|
|
|
# Inapplicable types will be ignored
|
2013-08-14 07:00:40 +08:00
|
|
|
#
|
|
|
|
# @argument module_item[completion_requirement][min_score] [Integer]
|
|
|
|
# Minimum score required to complete. Required for completion_requirement
|
|
|
|
# type 'min_score'.
|
2013-04-30 05:24:20 +08:00
|
|
|
#
|
|
|
|
# @example_request
|
|
|
|
#
|
|
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/modules/<module_id>/items \
|
|
|
|
# -X POST \
|
|
|
|
# -H 'Authorization: Bearer <token>' \
|
|
|
|
# -d 'module_item[title]=module item' \
|
|
|
|
# -d 'module_item[type]=ExternalTool' \
|
|
|
|
# -d 'module_item[content_id]=10' \
|
|
|
|
# -d 'module_item[position]=2' \
|
|
|
|
# -d 'module_item[indent]=1' \
|
|
|
|
# -d 'module_item[new_tab]=true'
|
|
|
|
#
|
|
|
|
# @returns Module Item
|
|
|
|
def create
|
|
|
|
@module = @context.context_modules.not_deleted.find(params[:module_id])
|
|
|
|
if authorized_action(@module, @current_user, :update)
|
|
|
|
return render :json => {:message => "missing module item parameter"}, :status => :bad_request unless params[:module_item]
|
|
|
|
|
|
|
|
item_params = params[:module_item].slice(:title, :type, :indent, :new_tab)
|
|
|
|
item_params[:id] = params[:module_item][:content_id]
|
2013-05-14 05:54:11 +08:00
|
|
|
if ['Page', 'WikiPage'].include?(item_params[:type])
|
|
|
|
if page_url = params[:module_item][:page_url]
|
|
|
|
if wiki_page = @context.wiki.wiki_pages.find_by_url(page_url)
|
|
|
|
item_params[:id] = wiki_page.id
|
|
|
|
else
|
|
|
|
return render :json => {:message => "invalid page_url parameter"}, :status => :bad_request
|
|
|
|
end
|
|
|
|
else
|
|
|
|
return render :json => {:message => "missing page_url parameter"}, :status => :bad_request
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-04-30 05:24:20 +08:00
|
|
|
item_params[:url] = params[:module_item][:external_url]
|
|
|
|
|
|
|
|
if (@tag = @module.add_item(item_params)) && set_position && set_completion_requirement
|
2013-05-15 03:53:14 +08:00
|
|
|
if @domain_root_account.enable_draft?
|
|
|
|
@tag.workflow_state = 'unpublished'
|
|
|
|
@tag.save
|
|
|
|
end
|
2013-04-30 05:24:20 +08:00
|
|
|
@module.touch
|
|
|
|
render :json => module_item_json(@tag, @current_user, session, @module, nil)
|
|
|
|
elsif @tag
|
|
|
|
render :json => @tag.errors.to_json, :status => :bad_request
|
|
|
|
else
|
|
|
|
render :status => 400, :json => { :message => t(:invalid_content, "Could not find content") }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @API Update a module item
|
|
|
|
#
|
|
|
|
# Update and return an existing module item
|
|
|
|
#
|
2013-08-14 07:00:40 +08:00
|
|
|
# @argument module_item[title] [Optional, String]
|
|
|
|
# The name of the module item
|
|
|
|
#
|
|
|
|
# @argument module_item[position] [Optional, Integer]
|
|
|
|
# The position of this item in the module (1-based)
|
|
|
|
#
|
|
|
|
# @argument module_item[indent] [Optional, Integer]
|
|
|
|
# 0-based indent level; module items may be indented to show a hierarchy
|
|
|
|
#
|
|
|
|
# @argument module_item[external_url] [Optional, String]
|
|
|
|
# External url that the item points to. Only applies to 'ExternalUrl' type.
|
|
|
|
#
|
|
|
|
# @argument module_item[new_tab] [Optional, Boolean]
|
|
|
|
# Whether the external tool opens in a new tab. Only applies to
|
|
|
|
# 'ExternalTool' type.
|
|
|
|
#
|
|
|
|
# @argument module_item[completion_requirement][type] [Optional, "must_view"|"must_contribute"|"must_submit"]
|
|
|
|
# Completion requirement for this module item.
|
2013-04-30 05:24:20 +08:00
|
|
|
# "must_view": Applies to all item types
|
|
|
|
# "must_contribute": Only applies to "Assignment", "Discussion", and "Page" types
|
|
|
|
# "must_submit", "min_score": Only apply to "Assignment" and "Quiz" types
|
|
|
|
# Inapplicable types will be ignored
|
2013-08-14 07:00:40 +08:00
|
|
|
#
|
|
|
|
# @argument module_item[completion_requirement][min_score] [Integer]
|
|
|
|
# Minimum score required to complete, Required for completion_requirement
|
|
|
|
# type 'min_score'.
|
|
|
|
#
|
|
|
|
# @argument module_item[published] [Optional, Boolean]
|
|
|
|
# Whether the module item is published and visible to students.
|
|
|
|
#
|
|
|
|
# @argument module_item[module_id] [Optional, String]
|
|
|
|
# Move this item to another module by specifying the target module id here.
|
|
|
|
# The target module must be in the same course.
|
2013-04-30 05:24:20 +08:00
|
|
|
#
|
|
|
|
# @example_request
|
|
|
|
#
|
|
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/modules/<module_id>/items/<item_id> \
|
|
|
|
# -X PUT \
|
|
|
|
# -H 'Authorization: Bearer <token>' \
|
|
|
|
# -d 'module_item[content_id]=10' \
|
|
|
|
# -d 'module_item[position]=2' \
|
|
|
|
# -d 'module_item[indent]=1' \
|
|
|
|
# -d 'module_item[new_tab]=true'
|
|
|
|
#
|
|
|
|
# @returns Module Item
|
|
|
|
def update
|
2013-05-15 03:53:14 +08:00
|
|
|
@tag = @context.context_module_tags.not_deleted.find(params[:id])
|
2013-04-30 05:24:20 +08:00
|
|
|
if authorized_action(@tag.context_module, @current_user, :update)
|
|
|
|
return render :json => {:message => "missing module item parameter"}, :status => :bad_request unless params[:module_item]
|
|
|
|
|
|
|
|
@tag.title = params[:module_item][:title] if params[:module_item][:title]
|
|
|
|
@tag.url = params[:module_item][:external_url] if %w(ExternalUrl ContextExternalTool).include?(@tag.content_type) && params[:module_item][:external_url]
|
|
|
|
@tag.indent = params[:module_item][:indent] if params[:module_item][:indent]
|
|
|
|
@tag.new_tab = value_to_boolean(params[:module_item][:new_tab]) if params[:module_item][:new_tab]
|
2013-07-30 23:12:49 +08:00
|
|
|
if target_module_id = params[:module_item][:module_id]
|
2013-08-06 05:13:51 +08:00
|
|
|
target_module = @context.context_modules.find_by_id(target_module_id)
|
|
|
|
return render :json => {:message => "invalid module_id"}, :status => :bad_request unless target_module
|
2013-07-30 23:12:49 +08:00
|
|
|
old_module = @context.context_modules.find(@tag.context_module_id)
|
2013-08-06 05:13:51 +08:00
|
|
|
@tag.context_module = target_module
|
2013-07-30 23:12:49 +08:00
|
|
|
if req_index = old_module.completion_requirements.find_index { |req| req[:id] == @tag.id }
|
|
|
|
old_module.completion_requirements_will_change!
|
|
|
|
req = old_module.completion_requirements.delete_at(req_index)
|
|
|
|
old_module.save!
|
|
|
|
params[:module_item][:completion_requirement] = req
|
|
|
|
else
|
|
|
|
ContentTag.touch_context_modules([old_module.id, target_module_id])
|
|
|
|
end
|
|
|
|
end
|
2013-04-30 05:24:20 +08:00
|
|
|
|
2013-05-15 03:53:14 +08:00
|
|
|
if params[:module_item].has_key?(:published)
|
|
|
|
if value_to_boolean(params[:module_item][:published])
|
|
|
|
@tag.publish
|
|
|
|
else
|
|
|
|
@tag.unpublish
|
|
|
|
end
|
2013-05-23 00:42:33 +08:00
|
|
|
@tag.save
|
2013-05-15 03:53:14 +08:00
|
|
|
@tag.update_asset_workflow_state!
|
|
|
|
@tag.context_module.save
|
|
|
|
end
|
|
|
|
|
2013-04-30 05:24:20 +08:00
|
|
|
if @tag.save && set_position && set_completion_requirement
|
|
|
|
@tag.update_asset_name! if params[:module_item][:title]
|
|
|
|
render :json => module_item_json(@tag, @current_user, session, @tag.context_module, nil)
|
|
|
|
else
|
|
|
|
render :json => @tag.errors.to_json, :status => :bad_request
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# @API Delete module item
|
|
|
|
#
|
|
|
|
# Delete a module item
|
|
|
|
#
|
|
|
|
# @example_request
|
|
|
|
#
|
|
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/modules/<module_id>/items/<item_id> \
|
|
|
|
# -X Delete \
|
|
|
|
# -H 'Authorization: Bearer <token>'
|
|
|
|
#
|
|
|
|
# @returns Module Item
|
|
|
|
def destroy
|
2013-05-15 03:53:14 +08:00
|
|
|
@tag = @context.context_module_tags.not_deleted.find(params[:id])
|
2013-04-30 05:24:20 +08:00
|
|
|
if authorized_action(@tag.context_module, @current_user, :update)
|
|
|
|
@module = @tag.context_module
|
|
|
|
@tag.destroy
|
|
|
|
@module.touch
|
|
|
|
render :json => module_item_json(@tag, @current_user, session, @module, nil)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def set_position
|
|
|
|
return true unless @tag && params[:module_item][:position]
|
|
|
|
|
|
|
|
@tag.reload
|
2013-05-15 03:53:14 +08:00
|
|
|
if @tag.insert_at_position(params[:module_item][:position], @tag.context_module.content_tags.not_deleted)
|
2013-04-30 05:24:20 +08:00
|
|
|
# see ContextModulesController#reorder_items
|
|
|
|
@tag.touch_context_module
|
|
|
|
ContentTag.update_could_be_locked(@tag.context_module.content_tags)
|
|
|
|
@context.touch
|
|
|
|
|
|
|
|
@tag.reload
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
@tag.errors.add(:position, t(:invalid_position, "Invalid position"))
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
2013-05-15 03:53:14 +08:00
|
|
|
protected :set_position
|
2013-04-30 05:24:20 +08:00
|
|
|
|
|
|
|
def set_completion_requirement
|
|
|
|
return true unless @tag && params[:module_item][:completion_requirement]
|
|
|
|
|
|
|
|
reqs = {}
|
|
|
|
@module ||= @tag.context_module
|
|
|
|
@module.completion_requirements.each{|i| reqs[i[:id]] = i }
|
|
|
|
|
|
|
|
if params[:module_item][:completion_requirement].blank?
|
|
|
|
reqs[@tag.id] = {}
|
|
|
|
elsif ["must_view", "must_submit", "must_contribute", "min_score"].include?(params[:module_item][:completion_requirement][:type])
|
|
|
|
reqs[@tag.id] = params[:module_item][:completion_requirement].with_indifferent_access
|
|
|
|
else
|
|
|
|
@tag.errors.add(:completion_requirement, t(:invalid_requirement_type, "Invalid completion requirement type"))
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
@module.completion_requirements = reqs
|
|
|
|
@module.save
|
|
|
|
end
|
2013-05-15 03:53:14 +08:00
|
|
|
protected :set_completion_requirement
|
2013-04-30 05:24:20 +08:00
|
|
|
end
|