2184 lines
87 KiB
Ruby
2184 lines
87 KiB
Ruby
#
|
|
# Copyright (C) 2011 - 2014 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 'set'
|
|
|
|
# @API Courses
|
|
# API for accessing course information.
|
|
#
|
|
# @model Term
|
|
# {
|
|
# "id": "Term",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "id": {
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "example": "Default Term",
|
|
# "type": "string"
|
|
# },
|
|
# "start_at": {
|
|
# "example": "2012-06-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "end_at": {
|
|
# "type": "datetime"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model CourseProgress
|
|
# {
|
|
# "id": "CourseProgress",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "requirement_count": {
|
|
# "description": "total number of requirements from all modules",
|
|
# "example": 10,
|
|
# "type": "integer"
|
|
# },
|
|
# "requirement_completed_count": {
|
|
# "description": "total number of requirements the user has completed from all modules",
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "next_requirement_url": {
|
|
# "description": "url to next module item that has an unmet requirement. null if the user has completed the course or the current module does not require sequential progress",
|
|
# "example": "http://localhost/courses/1/modules/items/2",
|
|
# "type": "string"
|
|
# },
|
|
# "completed_at": {
|
|
# "description": "date the course was completed. null if the course has not been completed by this user",
|
|
# "example": "2013-06-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model Course
|
|
# {
|
|
# "id": "Course",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "the unique identifier for the course",
|
|
# "example": 370663,
|
|
# "type": "integer"
|
|
# },
|
|
# "sis_course_id": {
|
|
# "description": "the SIS identifier for the course, if defined. This field is only included if the user has permission to view SIS information.",
|
|
# "type": "string"
|
|
# },
|
|
# "integration_id": {
|
|
# "description": "the integration identifier for the course, if defined. This field is only included if the user has permission to view SIS information.",
|
|
# "type": "string"
|
|
# },
|
|
# "name": {
|
|
# "description": "the full name of the course",
|
|
# "example": "InstructureCon 2012",
|
|
# "type": "string"
|
|
# },
|
|
# "course_code": {
|
|
# "description": "the course code",
|
|
# "example": "INSTCON12",
|
|
# "type": "string"
|
|
# },
|
|
# "workflow_state": {
|
|
# "description": "the current state of the course one of 'unpublished', 'available', 'completed', or 'deleted'",
|
|
# "example": "available",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "unpublished",
|
|
# "available",
|
|
# "completed",
|
|
# "deleted"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "account_id": {
|
|
# "description": "the account associated with the course",
|
|
# "example": 81259,
|
|
# "type": "integer"
|
|
# },
|
|
# "root_account_id": {
|
|
# "description": "the root account associated with the course",
|
|
# "example": 81259,
|
|
# "type": "integer"
|
|
# },
|
|
# "enrollment_term_id": {
|
|
# "description": "the enrollment term associated with the course",
|
|
# "example": 34,
|
|
# "type": "integer"
|
|
# },
|
|
# "start_at": {
|
|
# "description": "the start date for the course, if applicable",
|
|
# "example": "2012-06-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "end_at": {
|
|
# "description": "the end date for the course, if applicable",
|
|
# "example": "2012-09-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "enrollments": {
|
|
# "description": "A list of enrollments linking the current user to the course. for student enrollments, grading information may be included if include[]=total_scores",
|
|
# "type": "array",
|
|
# "items": { "$ref": "Enrollment" }
|
|
# },
|
|
# "calendar": {
|
|
# "description": "course calendar",
|
|
# "$ref": "CalendarLink"
|
|
# },
|
|
# "default_view": {
|
|
# "description": "the type of page that users will see when they first visit the course - 'feed': Recent Activity Dashboard - 'wiki': Wiki Front Page - 'modules': Course Modules/Sections Page - 'assignments': Course Assignments List - 'syllabus': Course Syllabus Page other types may be added in the future",
|
|
# "example": "feed",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "feed",
|
|
# "wiki",
|
|
# "modules",
|
|
# "syllabus",
|
|
# "assignments"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "syllabus_body": {
|
|
# "description": "optional: user-generated HTML for the course syllabus",
|
|
# "example": "<p>syllabus html goes here</p>",
|
|
# "type": "string"
|
|
# },
|
|
# "needs_grading_count": {
|
|
# "description": "optional: the number of submissions needing grading returned only if the current user has grading rights and include[]=needs_grading_count",
|
|
# "example": 17,
|
|
# "type": "integer"
|
|
# },
|
|
# "term": {
|
|
# "description": "optional: the enrollment term object for the course returned only if include[]=term",
|
|
# "$ref": "Term"
|
|
# },
|
|
# "course_progress": {
|
|
# "description": "optional (beta): information on progress through the course returned only if include[]=course_progress",
|
|
# "$ref": "CourseProgress"
|
|
# },
|
|
# "apply_assignment_group_weights": {
|
|
# "description": "weight final grade based on assignment group percentages",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "permissions": {
|
|
# "description": "optional: the permissions the user has for the course. returned only for a single course and include[]=permissions",
|
|
# "example": "{\"create_discussion_topic\"=>true}",
|
|
# "type": "map",
|
|
# "key": { "type": "string" },
|
|
# "value": { "type": "boolean" }
|
|
# },
|
|
# "is_public": {
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "public_syllabus": {
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "public_description": {
|
|
# "example": "Come one, come all to InstructureCon 2012!",
|
|
# "type": "string"
|
|
# },
|
|
# "storage_quota_mb": {
|
|
# "example": 5,
|
|
# "type": "integer"
|
|
# },
|
|
# "storage_quota_used_mb": {
|
|
# "example": 5,
|
|
# "type": "float"
|
|
# },
|
|
# "hide_final_grades": {
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "license": {
|
|
# "example": "Creative Commons",
|
|
# "type": "string"
|
|
# },
|
|
# "allow_student_assignment_edits": {
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "allow_wiki_comments": {
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "allow_student_forum_attachments": {
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "open_enrollment": {
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "self_enrollment": {
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "restrict_enrollments_to_course_dates": {
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "course_format": {
|
|
# "example": "online",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model CalendarLink
|
|
# {
|
|
# "id": "CalendarLink",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "ics": {
|
|
# "description": "The URL of the calendar in ICS format",
|
|
# "example": "https://canvas.instructure.com/feeds/calendars/course_abcdef.ics",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
class CoursesController < ApplicationController
|
|
include SearchHelper
|
|
|
|
before_filter :require_user, :only => [:index]
|
|
before_filter :require_context, :only => [:roster, :locks, :create_file, :ping]
|
|
skip_after_filter :update_enrollment_last_activity_at, only: [:enrollment_invitation]
|
|
|
|
include Api::V1::Course
|
|
include Api::V1::Progress
|
|
|
|
# @API List your courses
|
|
# Returns the list of active courses for the current user.
|
|
#
|
|
# @argument enrollment_type [String, "teacher"|"student"|"ta"|"observer"|"designer"]
|
|
# When set, only return courses where the user is enrolled as this type. For
|
|
# example, set to "teacher" to return only courses where the user is
|
|
# enrolled as a Teacher. This argument is ignored if enrollment_role is given.
|
|
#
|
|
# @argument enrollment_role [String] Deprecated
|
|
# When set, only return courses where the user is enrolled with the specified
|
|
# course-level role. This can be a role created with the
|
|
# {api:RoleOverridesController#add_role Add Role API} or a base role type of
|
|
# 'StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'ObserverEnrollment',
|
|
# or 'DesignerEnrollment'.
|
|
#
|
|
# @argument enrollment_role_id [Integer]
|
|
# When set, only return courses where the user is enrolled with the specified
|
|
# course-level role. This can be a role created with the
|
|
# {api:RoleOverridesController#add_role Add Role API} or a built_in role type of
|
|
# 'StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'ObserverEnrollment',
|
|
# or 'DesignerEnrollment'.
|
|
#
|
|
# @argument include[] [String, "needs_grading_count"|"syllabus_body"|"total_scores"|"term"|"course_progress"|"sections"|"storage_quota_used_mb"]
|
|
# - "needs_grading_count": Optional information to include with each Course.
|
|
# When needs_grading_count is given, and the current user has grading
|
|
# rights, the total number of submissions needing grading for all
|
|
# assignments is returned.
|
|
# - "syllabus_body": Optional information to include with each Course.
|
|
# When syllabus_body is given the user-generated html for the course
|
|
# syllabus is returned.
|
|
# - "total_scores": Optional information to include with each Course.
|
|
# When total_scores is given, any enrollments with type 'student' will also
|
|
# include the fields 'calculated_current_score', 'calculated_final_score',
|
|
# 'calculated_current_grade', and 'calculated_final_grade'.
|
|
# calculated_current_score is the student's score in the course, ignoring
|
|
# ungraded assignments. calculated_final_score is the student's score in
|
|
# the course including ungraded assignments with a score of 0.
|
|
# calculated_current_grade is the letter grade equivalent of
|
|
# calculated_current_score (if available). calculated_final_grade is the
|
|
# letter grade equivalent of calculated_final_score (if available). This
|
|
# argument is ignored if the course is configured to hide final grades.
|
|
# - "term": Optional information to include with each Course. When
|
|
# term is given, the information for the enrollment term for each course
|
|
# is returned.
|
|
# - "course_progress": Optional information to include with each Course.
|
|
# When course_progress is given, each course will include a
|
|
# 'course_progress' object with the fields: 'requirement_count', an integer
|
|
# specifying the total number of requirements in the course,
|
|
# 'requirement_completed_count', an integer specifying the total number of
|
|
# requirements in this course that have been completed, and
|
|
# 'next_requirement_url', a string url to the next requirement item, and
|
|
# 'completed_at', the date the course was completed (null if incomplete).
|
|
# 'next_requirement_url' will be null if all requirements have been
|
|
# completed or the current module does not require sequential progress.
|
|
# "course_progress" will return an error message if the course is not
|
|
# module based or the user is not enrolled as a student in the course.
|
|
# - "sections": Section enrollment information to include with each Course.
|
|
# Returns an array of hashes containing the section ID (id), section name
|
|
# (name), start and end dates (start_at, end_at), as well as the enrollment
|
|
# type (enrollment_role, e.g. 'StudentEnrollment').
|
|
# - "storage_quota_used_mb": The amount of storage space used by the files in this course
|
|
#
|
|
# @argument state[] [String, "unpublished"|"available"|"completed"|"deleted"]
|
|
# If set, only return courses that are in the given state(s).
|
|
# By default, "available" is returned for students and observers, and
|
|
# anything except "deleted", for all other enrollment types
|
|
#
|
|
# @returns [Course]
|
|
def index
|
|
respond_to do |format|
|
|
format.html {
|
|
all_enrollments = @current_user.enrollments.with_each_shard { |scope| scope.not_deleted }
|
|
@past_enrollments = []
|
|
@current_enrollments = []
|
|
@future_enrollments = []
|
|
Canvas::Builders::EnrollmentDateBuilder.preload(all_enrollments)
|
|
all_enrollments.each do |e|
|
|
if [:completed, :rejected].include?(e.state_based_on_date)
|
|
@past_enrollments << e
|
|
else
|
|
start_at, end_at = e.enrollment_dates.first
|
|
if start_at && start_at > Time.now.utc
|
|
@future_enrollments << e unless %w(StudentEnrollment ObserverEnrollment).include?(e.type) && e.root_account.settings[:restrict_student_future_view]
|
|
else
|
|
@current_enrollments << e
|
|
end
|
|
end
|
|
end
|
|
|
|
@visible_groups = @current_user.visible_groups
|
|
|
|
@past_enrollments.sort_by!{|e| Canvas::ICU.collation_key(e.long_name)}
|
|
[@current_enrollments, @future_enrollments].each{|list| list.sort_by!{|e| [e.active? ? 1 : 0, Canvas::ICU.collation_key(e.long_name)] }}
|
|
}
|
|
|
|
format.json {
|
|
if params[:state]
|
|
states = Array(params[:state])
|
|
states += %w(created claimed) if states.include?('unpublished')
|
|
conditions = states.map{ |state|
|
|
Enrollment::QueryBuilder.new(nil, course_workflow_state: state, enforce_course_workflow_state: true).conditions
|
|
}.compact.join(" OR ")
|
|
enrollments = @current_user.enrollments.joins(:course).includes(:course).where(conditions)
|
|
else
|
|
enrollments = @current_user.cached_current_enrollments(preload_courses: true)
|
|
end
|
|
|
|
# TODO: preload roles after enrollment#role shim is taken out
|
|
if params[:enrollment_role]
|
|
enrollments = enrollments.reject { |e| e.role.name != params[:enrollment_role] }
|
|
elsif params[:enrollment_role_id]
|
|
enrollments = enrollments.reject { |e| e.role.id.to_s != params[:enrollment_role_id].to_s }
|
|
elsif params[:enrollment_type]
|
|
e_type = "#{params[:enrollment_type].capitalize}Enrollment"
|
|
enrollments = enrollments.reject { |e| e.class.name != e_type }
|
|
end
|
|
|
|
if value_to_boolean(params[:current_domain_only])
|
|
enrollments = enrollments.select { |e| e.root_account_id == @domain_root_account.id }
|
|
elsif params[:root_account_id]
|
|
root_account = api_find_all(Account, [params[:root_account_id]], { limit: 1 }).first
|
|
enrollments = root_account ? enrollments.select { |e| e.root_account_id == root_account.id } : []
|
|
end
|
|
|
|
includes = Set.new(Array(params[:include]))
|
|
|
|
# We only want to return the permissions for single courses and not lists of courses.
|
|
includes.delete 'permissions'
|
|
|
|
hash = []
|
|
enrollments_by_course = enrollments.group_by(&:course_id).values
|
|
enrollments_by_course = Api.paginate(enrollments_by_course, self, api_v1_courses_url) if api_request?
|
|
enrollments_by_course.each do |course_enrollments|
|
|
course = course_enrollments.first.course
|
|
hash << course_json(course, @current_user, session, includes, course_enrollments)
|
|
end
|
|
render :json => hash
|
|
}
|
|
end
|
|
end
|
|
|
|
# @API Create a new course
|
|
# Create a new course
|
|
#
|
|
# @argument account_id [Required, Integer]
|
|
# The unique ID of the account to create to course under.
|
|
#
|
|
# @argument course[name] [String]
|
|
# The name of the course. If omitted, the course will be named "Unnamed
|
|
# Course."
|
|
#
|
|
# @argument course[course_code] [String]
|
|
# The course code for the course.
|
|
#
|
|
# @argument course[start_at] [DateTime]
|
|
# Course start date in ISO8601 format, e.g. 2011-01-01T01:00Z
|
|
#
|
|
# @argument course[end_at] [DateTime]
|
|
# Course end date in ISO8601 format. e.g. 2011-01-01T01:00Z
|
|
#
|
|
# @argument course[license] [String]
|
|
# The name of the licensing. Should be one of the following abbreviations
|
|
# (a descriptive name is included in parenthesis for reference):
|
|
# - 'private' (Private Copyrighted)
|
|
# - 'cc_by_nc_nd' (CC Attribution Non-Commercial No Derivatives)
|
|
# - 'cc_by_nc_sa' (CC Attribution Non-Commercial Share Alike)
|
|
# - 'cc_by_nc' (CC Attribution Non-Commercial)
|
|
# - 'cc_by_nd' (CC Attribution No Derivatives)
|
|
# - 'cc_by_sa' (CC Attribution Share Alike)
|
|
# - 'cc_by' (CC Attribution)
|
|
# - 'public_domain' (Public Domain).
|
|
#
|
|
# @argument course[is_public] [Boolean]
|
|
# Set to true if course if public.
|
|
#
|
|
# @argument course[public_syllabus] [Boolean]
|
|
# Set to true to make the course syllabus public.
|
|
#
|
|
# @argument course[public_description] [String]
|
|
# A publicly visible description of the course.
|
|
#
|
|
# @argument course[allow_student_wiki_edits] [Boolean]
|
|
# If true, students will be able to modify the course wiki.
|
|
#
|
|
# @argument course[allow_wiki_comments] [Boolean]
|
|
# If true, course members will be able to comment on wiki pages.
|
|
#
|
|
# @argument course[allow_student_forum_attachments] [Boolean]
|
|
# If true, students can attach files to forum posts.
|
|
#
|
|
# @argument course[open_enrollment] [Boolean]
|
|
# Set to true if the course is open enrollment.
|
|
#
|
|
# @argument course[self_enrollment] [Boolean]
|
|
# Set to true if the course is self enrollment.
|
|
#
|
|
# @argument course[restrict_enrollments_to_course_dates] [Boolean]
|
|
# Set to true to restrict user enrollments to the start and end dates of the
|
|
# course.
|
|
#
|
|
# @argument course[term_id] [Integer]
|
|
# The unique ID of the term to create to course in.
|
|
#
|
|
# @argument course[sis_course_id] [String]
|
|
# The unique SIS identifier.
|
|
#
|
|
# @argument course[integration_id] [String]
|
|
# The unique Integration identifier.
|
|
#
|
|
# @argument course[hide_final_grades] [Boolean]
|
|
# If this option is set to true, the totals in student grades summary will
|
|
# be hidden.
|
|
#
|
|
# @argument course[apply_assignment_group_weights] [Boolean]
|
|
# Set to true to weight final grade based on assignment groups percentages.
|
|
#
|
|
# @argument offer [Boolean]
|
|
# If this option is set to true, the course will be available to students
|
|
# immediately.
|
|
#
|
|
# @argument enroll_me [Boolean]
|
|
# Set to true to enroll the current user as the teacher.
|
|
#
|
|
# @argument course[syllabus_body] [String]
|
|
# The syllabus body for the course
|
|
#
|
|
# @argument course[grading_standard_id] [Integer]
|
|
# The grading standard id to set for the course. If no value is provided for this argument the current grading_standard will be un-set from this course.
|
|
#
|
|
# @argument course[course_format] [String]
|
|
# Optional. Specifies the format of the course. (Should be either 'on_campus' or 'online')
|
|
#
|
|
# @returns Course
|
|
def create
|
|
@account = params[:account_id] ? api_find(Account, params[:account_id]) : @domain_root_account.manually_created_courses_account
|
|
if authorized_action(@account, @current_user, [:manage_courses, :create_courses])
|
|
params[:course] ||= {}
|
|
|
|
if params[:course].has_key?(:syllabus_body)
|
|
params[:course][:syllabus_body] = process_incoming_html_content(params[:course][:syllabus_body])
|
|
end
|
|
|
|
if (sub_account_id = params[:course].delete(:account_id)) && sub_account_id.to_i != @account.id
|
|
@sub_account = @account.find_child(sub_account_id) || raise(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
term_id = params[:course].delete(:term_id).presence || params[:course].delete(:enrollment_term_id).presence
|
|
params[:course][:enrollment_term] = api_find(@account.root_account.enrollment_terms, term_id) if term_id
|
|
|
|
sis_course_id = params[:course].delete(:sis_course_id)
|
|
apply_assignment_group_weights = params[:course].delete(:apply_assignment_group_weights)
|
|
|
|
# accept end_at as an alias for conclude_at. continue to accept
|
|
# conclude_at for legacy support, and return conclude_at only if
|
|
# the user uses that name.
|
|
course_end = if params[:course][:end_at].present?
|
|
params[:course][:conclude_at] = params[:course].delete(:end_at)
|
|
:end_at
|
|
else
|
|
:conclude_at
|
|
end
|
|
|
|
unless @account.grants_right? @current_user, session, :manage_storage_quotas
|
|
params[:course].delete :storage_quota
|
|
params[:course].delete :storage_quota_mb
|
|
end
|
|
|
|
@course = (@sub_account || @account).courses.build(params[:course])
|
|
if api_request? && @account.grants_right?(@current_user, :manage_sis)
|
|
@course.sis_source_id = sis_course_id
|
|
end
|
|
|
|
if apply_assignment_group_weights
|
|
@course.apply_assignment_group_weights = value_to_boolean apply_assignment_group_weights
|
|
end
|
|
|
|
changes = changed_settings(@course.changes, @course.settings)
|
|
|
|
respond_to do |format|
|
|
if @course.save
|
|
Auditors::Course.record_created(@course, @current_user, changes, source: (api_request? ? :api : :manual))
|
|
@course.enroll_user(@current_user, 'TeacherEnrollment', :enrollment_state => 'active') if params[:enroll_me].to_s == 'true'
|
|
@course.require_assignment_group rescue nil
|
|
# offer updates the workflow state, saving the record without doing validation callbacks
|
|
if api_request? and params[:offer].present?
|
|
@course.offer
|
|
Auditors::Course.record_published(@course, @current_user, source: :api)
|
|
end
|
|
format.html { redirect_to @course }
|
|
format.json { render :json => course_json(
|
|
@course,
|
|
@current_user,
|
|
session,
|
|
[:start_at, course_end, :license,
|
|
:is_public, :public_syllabus, :allow_student_assignment_edits, :allow_wiki_comments,
|
|
:allow_student_forum_attachments, :open_enrollment, :self_enrollment,
|
|
:root_account_id, :account_id, :public_description,
|
|
:restrict_enrollments_to_course_dates, :hide_final_grades], nil)
|
|
}
|
|
else
|
|
flash[:error] = t('errors.create_failed', "Course creation failed")
|
|
format.html { redirect_to :root_url }
|
|
format.json { render :json => @course.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Upload a file
|
|
#
|
|
# Upload a file to the course.
|
|
#
|
|
# This API endpoint is the first step in uploading a file to a course.
|
|
# See the {file:file_uploads.html File Upload Documentation} for details on
|
|
# the file upload workflow.
|
|
#
|
|
# Only those with the "Manage Files" permission on a course can upload files
|
|
# to the course. By default, this is Teachers, TAs and Designers.
|
|
def create_file
|
|
@attachment = Attachment.new(:context => @context)
|
|
if authorized_action(@attachment, @current_user, :create)
|
|
api_attachment_preflight(@context, request, :check_quota => true)
|
|
end
|
|
end
|
|
|
|
def backup
|
|
get_context
|
|
if authorized_action(@context, @current_user, :update)
|
|
backup_json = @context.backup_to_json
|
|
send_file_headers!( :length=>backup_json.length, :filename=>"#{@context.name.underscore.gsub(/\s/, "_")}_#{Time.zone.today.to_s}_backup.instructure", :disposition => 'attachment', :type => 'application/instructure')
|
|
render :text => proc {|response, output|
|
|
output.write backup_json
|
|
}
|
|
end
|
|
end
|
|
|
|
def restore
|
|
get_context
|
|
if authorized_action(@context, @current_user, :update)
|
|
respond_to do |format|
|
|
if params[:restore]
|
|
@context.restore_from_json_backup(params[:restore])
|
|
flash[:notice] = t('notices.backup_restored', "Backup Successfully Restored!")
|
|
format.html { redirect_to named_context_url(@context, :context_url) }
|
|
else
|
|
format.html
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def unconclude
|
|
get_context
|
|
if authorized_action(@context, @current_user, :change_course_state)
|
|
@context.unconclude
|
|
Auditors::Course.record_unconcluded(@context, @current_user, source: (api_request? ? :api : :manual))
|
|
flash[:notice] = t('notices.unconcluded', "Course un-concluded")
|
|
redirect_to(named_context_url(@context, :context_url))
|
|
end
|
|
end
|
|
|
|
include Api::V1::User
|
|
|
|
# @API List students
|
|
#
|
|
# Returns the list of students enrolled in this course.
|
|
#
|
|
# DEPRECATED: Please use the {api:CoursesController#users course users} endpoint
|
|
# and pass "student" as the enrollment_type.
|
|
#
|
|
# @returns [User]
|
|
def students
|
|
# DEPRECATED. Needs to stay separate from #users though, because this is un-paginated
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_roster)
|
|
proxy = @context.students.order_by_sortable_name
|
|
user_json_preloads(proxy, false)
|
|
render :json => proxy.map { |u| user_json(u, @current_user, session) }
|
|
end
|
|
end
|
|
|
|
# @API List users in course
|
|
# Returns the list of users in this course. And optionally the user's enrollments in the course.
|
|
#
|
|
# @argument search_term [String]
|
|
# The partial name or full ID of the users to match and return in the results list.
|
|
#
|
|
# @argument enrollment_type [String, "teacher"|"student"|"ta"|"observer"|"designer"]
|
|
# When set, only return users where the user is enrolled as this type.
|
|
# This argument is ignored if enrollment_role is given.
|
|
#
|
|
# @argument enrollment_role [String] Deprecated
|
|
# When set, only return users enrolled with the specified course-level role. This can be
|
|
# a role created with the {api:RoleOverridesController#add_role Add Role API} or a
|
|
# base role type of 'StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment',
|
|
# 'ObserverEnrollment', or 'DesignerEnrollment'.
|
|
#
|
|
# @argument enrollment_role_id [Integer]
|
|
# When set, only return courses where the user is enrolled with the specified
|
|
# course-level role. This can be a role created with the
|
|
# {api:RoleOverridesController#add_role Add Role API} or a built_in role id with type
|
|
# 'StudentEnrollment', 'TeacherEnrollment', 'TaEnrollment', 'ObserverEnrollment',
|
|
# or 'DesignerEnrollment'.
|
|
#
|
|
# @argument include[] [String, "email"|"enrollments"|"locked"|"avatar_url"|"test_student"|"bio"]
|
|
# - "email": Optional user email.
|
|
# - "enrollments":
|
|
# Optionally include with each Course the user's current and invited
|
|
# enrollments. If the user is enrolled as a student, and the account has
|
|
# permission to manage or view all grades, each enrollment will include a
|
|
# 'grades' key with 'current_score', 'final_score', 'current_grade' and
|
|
# 'final_grade' values.
|
|
# - "locked": Optionally include whether an enrollment is locked.
|
|
# - "avatar_url": Optionally include avatar_url.
|
|
# - "bio": Optionally include each user's bio.
|
|
# - "test_student": Optionally include the course's Test Student,
|
|
# if present. Default is to not include Test Student.
|
|
#
|
|
# @argument user_id [String]
|
|
# If included, the user will be queried and if the user is part of the
|
|
# users set, the page parameter will be modified so that the page
|
|
# containing user_id will be returned.
|
|
#
|
|
# @argument enrollment_state[] [String, "active"|"invited"|"rejected"|"completed"|"inactive"]
|
|
# When set, only return users where the enrollment workflow state is of one of the given types.
|
|
# "active" and "invited" enrollments are returned by default.
|
|
# @returns [User]
|
|
def users
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_roster)
|
|
#backcompat limit param
|
|
params[:per_page] ||= params[:limit]
|
|
|
|
search_params = params.slice(:search_term, :enrollment_role, :enrollment_role_id, :enrollment_type, :enrollment_state)
|
|
search_term = search_params[:search_term].presence
|
|
|
|
if search_term
|
|
users = UserSearch.for_user_in_context(search_term, @context, @current_user, session, search_params)
|
|
else
|
|
users = UserSearch.scope_for(@context, @current_user, search_params)
|
|
end
|
|
# If a user_id is passed in, modify the page parameter so that the page
|
|
# that contains that user is returned.
|
|
# We delete it from params so that it is not maintained in pagination links.
|
|
user_id = params[:user_id]
|
|
if user_id.present? && user = users.where(:users => { :id => user_id }).first
|
|
position_scope = users.where("#{User.sortable_name_order_by_clause}<=#{User.best_unicode_collation_key('?')}", user.sortable_name)
|
|
position = position_scope.count(:select => "users.id", :distinct => true)
|
|
per_page = Api.per_page_for(self)
|
|
params[:page] = (position.to_f / per_page.to_f).ceil
|
|
end
|
|
|
|
users = Api.paginate(users, self, api_v1_course_users_url)
|
|
includes = Array(params[:include])
|
|
user_json_preloads(users, includes.include?('email'))
|
|
if not includes.include?('test_student')
|
|
users.reject! do |u|
|
|
u.preferences[:fake_student]
|
|
end
|
|
end
|
|
if includes.include?('enrollments')
|
|
# not_ended_enrollments for enrollment_json
|
|
# enrollments course for has_grade_permissions?
|
|
ActiveRecord::Associations::Preloader.new(users,
|
|
{ :not_ended_enrollments => :course },
|
|
:conditions => ['enrollments.course_id = ?', @context.id]).run
|
|
end
|
|
render :json => users.map { |u|
|
|
enrollments = u.not_ended_enrollments if includes.include?('enrollments')
|
|
user_json(u, @current_user, session, includes, @context, enrollments)
|
|
}
|
|
end
|
|
end
|
|
|
|
# @API List recently logged in students
|
|
#
|
|
# Returns the list of users in this course, ordered by how recently they have
|
|
# logged in. The records include the 'last_login' field which contains
|
|
# a timestamp of the last time that user logged into canvas. The querying
|
|
# user must have the 'View usage reports' permission.
|
|
#
|
|
# @example_request
|
|
# curl -H 'Authorization: Bearer <token>' \
|
|
# https://<canvas>/api/v1/courses/<course_id>/recent_users
|
|
#
|
|
# @returns [User]
|
|
def recent_students
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_reports)
|
|
scope = User.for_course_with_last_login(@context, @context.root_account_id, 'StudentEnrollment')
|
|
scope = scope.order('login_info_exists, last_login DESC')
|
|
users = Api.paginate(scope, self, api_v1_course_recent_students_url)
|
|
user_json_preloads(users)
|
|
render :json => users.map { |u| user_json(u, @current_user, session, ['last_login']) }
|
|
end
|
|
end
|
|
|
|
# @API Get single user
|
|
# Return information on a single user.
|
|
#
|
|
# Accepts the same include[] parameters as the :users: action, and returns a
|
|
# single user with the same fields as that action.
|
|
#
|
|
# @returns User
|
|
def user
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_roster)
|
|
users = api_find_all(@context.users_visible_to(@current_user), [params[:id]])
|
|
includes = Array(params[:include])
|
|
user_json_preloads(users, includes.include?('email'))
|
|
if includes.include?('enrollments')
|
|
# not_ended_enrollments for enrollment_json
|
|
# enrollments course for has_grade_permissions?
|
|
ActiveRecord::Associations::Preloader.new(users,
|
|
{:not_ended_enrollments => :course},
|
|
:conditions => ['enrollments.course_id = ?', @context.id]).run
|
|
end
|
|
user = users.first or raise ActiveRecord::RecordNotFound
|
|
enrollments = user.not_ended_enrollments if includes.include?('enrollments')
|
|
render :json => user_json(user, @current_user, session, includes, @context, enrollments)
|
|
end
|
|
end
|
|
|
|
include Api::V1::PreviewHtml
|
|
# @API Preview processed html
|
|
#
|
|
# Preview html content processed for this course
|
|
#
|
|
# @argument html The html content to process
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/preview_html \
|
|
# -F 'html=<p><badhtml></badhtml>processed html</p>' \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "html": "<p>processed html</p>"
|
|
# }
|
|
def preview_html
|
|
get_context
|
|
if @context && authorized_action(@context, @current_user, :read)
|
|
render_preview_html
|
|
end
|
|
end
|
|
|
|
include Api::V1::StreamItem
|
|
# @API Course activity stream
|
|
# Returns the current user's course-specific activity stream, paginated.
|
|
#
|
|
# For full documentation, see the API documentation for the user activity
|
|
# stream, in the user api.
|
|
def activity_stream
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read)
|
|
api_render_stream(contexts: [@context], paginate_url: :api_v1_course_activity_stream_url)
|
|
end
|
|
end
|
|
|
|
# @API Course activity stream summary
|
|
# Returns a summary of the current user's course-specific activity stream.
|
|
#
|
|
# For full documentation, see the API documentation for the user activity
|
|
# stream summary, in the user api.
|
|
def activity_stream_summary
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read)
|
|
api_render_stream_summary([@context])
|
|
end
|
|
end
|
|
|
|
include Api::V1::TodoItem
|
|
# @API Course TODO items
|
|
# Returns the current user's course-specific todo items.
|
|
#
|
|
# For full documentation, see the API documentation for the user todo items, in the user api.
|
|
def todo_items
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read)
|
|
grading = @current_user.assignments_needing_grading(:contexts => [@context]).map { |a| todo_item_json(a, @current_user, session, 'grading') }
|
|
submitting = @current_user.assignments_needing_submitting(:contexts => [@context]).map { |a| todo_item_json(a, @current_user, session, 'submitting') }
|
|
render :json => (grading + submitting)
|
|
end
|
|
end
|
|
|
|
# @API Conclude a course
|
|
# Delete or conclude an existing course
|
|
#
|
|
# @argument event [Required, String, "delete"|"conclude"]
|
|
# The action to take on the course.
|
|
def destroy
|
|
@context = api_find(Course, params[:id])
|
|
if api_request? && !['delete', 'conclude'].include?(params[:event])
|
|
return render(:json => { :message => 'Only "delete" and "conclude" events are allowed.' }, :status => :bad_request)
|
|
end
|
|
if params[:event] != 'conclude' && (@context.created? || @context.claimed? || params[:event] == 'delete')
|
|
return unless authorized_action(@context, @current_user, permission_for_event(params[:event]))
|
|
@context.destroy
|
|
Auditors::Course.record_deleted(@context, @current_user, source: (api_request? ? :api : :manual))
|
|
flash[:notice] = t('notices.deleted', "Course successfully deleted")
|
|
else
|
|
return unless authorized_action(@context, @current_user, permission_for_event(params[:event]))
|
|
|
|
@context.complete
|
|
if @context.save
|
|
Auditors::Course.record_concluded(@context, @current_user, source: (api_request? ? :api : :manual))
|
|
flash[:notice] = t('notices.concluded', "Course successfully concluded")
|
|
else
|
|
flash[:notice] = t('notices.failed_conclude', "Course failed to conclude")
|
|
end
|
|
end
|
|
@current_user.touch
|
|
respond_to do |format|
|
|
format.html { redirect_to dashboard_url }
|
|
format.json {
|
|
render :json => { params[:event] => true }
|
|
}
|
|
end
|
|
end
|
|
|
|
def statistics
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_reports)
|
|
@student_ids = @context.student_ids
|
|
@range_start = Date.parse("Jan 1 2000")
|
|
@range_end = Date.tomorrow
|
|
|
|
query = "SELECT COUNT(id), SUM(size) FROM attachments WHERE context_id=%s AND context_type='Course' AND root_attachment_id IS NULL AND file_state != 'deleted'"
|
|
row = Attachment.connection.select_rows(query % [@context.id]).first
|
|
@file_count, @files_size = [row[0].to_i, row[1].to_i]
|
|
query = "SELECT COUNT(id), SUM(max_size) FROM media_objects WHERE context_id=%s AND context_type='Course' AND attachment_id IS NULL AND workflow_state != 'deleted'"
|
|
row = MediaObject.connection.select_rows(query % [@context.id]).first
|
|
@media_file_count, @media_files_size = [row[0].to_i, row[1].to_i]
|
|
|
|
if params[:range] && params[:date]
|
|
date = Date.parse(params[:date]) rescue nil
|
|
date ||= Time.zone.today
|
|
if params[:range] == 'week'
|
|
@view_week = (date - 1) - (date - 1).wday + 1
|
|
@range_start = @view_week
|
|
@range_end = @view_week + 6
|
|
@old_range_start = @view_week - 7.days
|
|
elsif params[:range] == 'month'
|
|
@view_month = Date.new(date.year, date.month, d=1) #view.created_at.strftime("%m:%Y")
|
|
@range_start = @view_month
|
|
@range_end = (@view_month >> 1) - 1
|
|
@old_range_start = @view_month << 1
|
|
end
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
js_env(:RECENT_STUDENTS_URL => api_v1_course_recent_students_url(@context))
|
|
end
|
|
format.json { render :json => @categories }
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Get course settings
|
|
# Returns some of a course's settings.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/settings \
|
|
# -X GET \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "allow_student_discussion_topics": true,
|
|
# "allow_student_forum_attachments": false,
|
|
# "allow_student_discussion_editing": true,
|
|
# "grading_standard_enabled": true,
|
|
# "grading_standard_id": 137,
|
|
# "allow_student_organized_groups": true,
|
|
# "hide_final_grades": false,
|
|
# "hide_distribution_graphs": false,
|
|
# "lock_all_announcements": true
|
|
# }
|
|
def settings
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read_as_admin)
|
|
if api_request?
|
|
render :json => course_settings_json(@context)
|
|
return
|
|
end
|
|
|
|
load_all_contexts(:context => @context)
|
|
|
|
@all_roles = Role.custom_roles_and_counts_for_course(@context, @current_user, true)
|
|
|
|
@invited_count = @context.invited_count_visible_to(@current_user)
|
|
|
|
js_env(:COURSE_ID => @context.id,
|
|
:USERS_URL => "/api/v1/courses/#{ @context.id }/users",
|
|
:ALL_ROLES => @all_roles,
|
|
:COURSE_ROOT_URL => "/courses/#{ @context.id }",
|
|
:SEARCH_URL => search_recipients_url,
|
|
:CONTEXTS => @contexts,
|
|
:USER_PARAMS => {:include => ['email', 'enrollments', 'locked', 'observed_users']},
|
|
:PERMISSIONS => {
|
|
:manage_students => @context.grants_right?(@current_user, session, :manage_students),
|
|
:manage_admin_users => @context.grants_right?(@current_user, session, :manage_admin_users),
|
|
:manage_account_settings => @context.account.grants_right?(@current_user, session, :manage_account_settings),
|
|
})
|
|
|
|
@alerts = @context.alerts
|
|
add_crumb(t('#crumbs.settings', "Settings"), named_context_url(@context, :context_details_url))
|
|
js_env :APP_CENTER => {
|
|
enabled: Canvas::Plugin.find(:app_center).enabled?
|
|
}
|
|
|
|
@course_settings_sub_navigation_tools = ContextExternalTool.all_tools_for(@context, :type => :course_settings_sub_navigation, :root_account => @domain_root_account, :current_user => @current_user)
|
|
unless @context.grants_right?(@current_user, session, :manage_content)
|
|
@course_settings_sub_navigation_tools.reject! { |tool| tool.course_settings_sub_navigation(:visibility) == 'admins' }
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Update course settings
|
|
# Can update the following course settings:
|
|
#
|
|
# @argument allow_student_discussion_topics [Boolean]
|
|
# Let students create discussion topics
|
|
#
|
|
# @argument allow_student_forum_attachments [Boolean]
|
|
# Let students attach files to discussions
|
|
#
|
|
# @argument allow_student_discussion_editing [Boolean]
|
|
# Let students edit or delete their own discussion posts
|
|
#
|
|
# @argument allow_student_organized_groups [Boolean]
|
|
# Let students organize their own groups
|
|
#
|
|
# @argument hide_final_grades [Boolean]
|
|
# Hide totals in student grades summary
|
|
#
|
|
# @argument hide_distribution_graphs [Boolean]
|
|
# Hide grade distribution graphs from students
|
|
#
|
|
# @argument lock_all_announcements [Boolean]
|
|
# Disable comments on announcements
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/settings \
|
|
# -X PUT \
|
|
# -H 'Authorization: Bearer <token>' \
|
|
# -d 'allow_student_discussion_topics=false'
|
|
def update_settings
|
|
return unless api_request?
|
|
@course = api_find(Course, params[:course_id])
|
|
return unless authorized_action(@course, @current_user, :update)
|
|
|
|
old_settings = @course.settings
|
|
@course.attributes = params.slice(
|
|
:allow_student_discussion_topics,
|
|
:allow_student_forum_attachments,
|
|
:allow_student_discussion_editing,
|
|
:show_total_grade_as_points,
|
|
:allow_student_organized_groups,
|
|
:hide_final_grades,
|
|
:hide_distribution_graphs,
|
|
:lock_all_announcements
|
|
)
|
|
changes = changed_settings(@course.changes, @course.settings, old_settings)
|
|
|
|
if @course.save
|
|
Auditors::Course.record_updated(@course, @current_user, changes, source: :api)
|
|
render :json => course_settings_json(@course)
|
|
else
|
|
render :json => @course.errors, :status => :bad_request
|
|
end
|
|
end
|
|
|
|
def update_nav
|
|
get_context
|
|
if authorized_action(@context, @current_user, :update)
|
|
@context.tab_configuration = JSON.parse(params[:tabs_json])
|
|
@context.save
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_details_url) }
|
|
format.json { render :json => {:update_nav => true} }
|
|
end
|
|
end
|
|
end
|
|
|
|
def roster
|
|
if authorized_action(@context, @current_user, :read_roster)
|
|
log_asset_access("roster:#{@context.asset_string}", "roster", "other")
|
|
@students = @context.participating_students.order_by_sortable_name
|
|
@teachers = @context.instructors.order_by_sortable_name
|
|
@groups = @context.groups.active
|
|
end
|
|
end
|
|
|
|
def re_send_invitations
|
|
get_context
|
|
if authorized_action(@context, @current_user, [:manage_students, :manage_admin_users])
|
|
@context.send_later_if_production(:re_send_invitations!)
|
|
|
|
respond_to do |format|
|
|
format.html { redirect_to course_settings_url }
|
|
format.json { render :json => {:re_sent => true} }
|
|
end
|
|
end
|
|
end
|
|
|
|
def enrollment_invitation
|
|
get_context
|
|
|
|
return if check_enrollment(true)
|
|
return !!redirect_to(course_url(@context.id)) unless @pending_enrollment
|
|
|
|
if params[:reject]
|
|
return reject_enrollment(@pending_enrollment)
|
|
elsif params[:accept]
|
|
return accept_enrollment(@pending_enrollment)
|
|
else
|
|
redirect_to course_url(@context.id)
|
|
end
|
|
end
|
|
|
|
# Internal: Accept an enrollment invitation and redirect.
|
|
#
|
|
# enrollment - An enrollment object to accept.
|
|
#
|
|
# Returns nothing.
|
|
def accept_enrollment(enrollment)
|
|
if @current_user && enrollment.user == @current_user
|
|
if enrollment.workflow_state == 'invited'
|
|
enrollment.accept!
|
|
flash[:notice] = t('notices.invitation_accepted', 'Invitation accepted! Welcome to %{course}!', :course => @context.name)
|
|
end
|
|
|
|
session[:accepted_enrollment_uuid] = enrollment.uuid
|
|
|
|
if params[:action] != 'show'
|
|
if @context.restrict_enrollments_to_course_dates?
|
|
redirect_to courses_url
|
|
else
|
|
redirect_to course_url(@context.id)
|
|
end
|
|
else
|
|
@context_enrollment = enrollment
|
|
enrollment = nil
|
|
return false
|
|
end
|
|
elsif !@current_user && enrollment.user.registered? || !enrollment.user.email_channel
|
|
session[:return_to] = course_url(@context.id)
|
|
flash[:notice] = t('notices.login_to_accept', "You'll need to log in before you can accept the enrollment.")
|
|
return redirect_to login_url(:force_login => 1) if @current_user
|
|
redirect_to login_url
|
|
else
|
|
# defer to CommunicationChannelsController#confirm for the logic of merging users
|
|
redirect_to registration_confirmation_path(enrollment.user.email_channel.confirmation_code, :enrollment => enrollment.uuid)
|
|
end
|
|
end
|
|
protected :accept_enrollment
|
|
|
|
# Internal: Reject an enrollment invitation and redirect.
|
|
#
|
|
# enrollment - An enrollment object to reject.
|
|
#
|
|
# Returns nothing.
|
|
def reject_enrollment(enrollment)
|
|
if enrollment.invited?
|
|
enrollment.reject!
|
|
flash[:notice] = t('notices.invitation_cancelled', 'Invitation canceled.')
|
|
end
|
|
|
|
session.delete(:enrollment_uuid)
|
|
session[:permissions_key] = CanvasUUID.generate
|
|
|
|
redirect_to(@current_user ? dashboard_url : root_url)
|
|
end
|
|
protected :reject_enrollment
|
|
|
|
def claim_course
|
|
if params[:verification] == @context.uuid
|
|
session[:claim_course_uuid] = @context.uuid
|
|
# session[:course_uuid] = @context.uuid
|
|
end
|
|
if session[:claim_course_uuid] == @context.uuid && @current_user && @context.state == :created
|
|
claim_session_course(@context, @current_user)
|
|
end
|
|
end
|
|
protected :claim_course
|
|
|
|
# Protected: Check a user's enrollment in the current course and redirect
|
|
# them/clean up the session as needed.
|
|
#
|
|
# ignore_restricted_courses - if true, don't exit on enrollments to non-active,
|
|
# date-restricted courses.
|
|
#
|
|
# Returns boolean (true if parent request should be cancelled).
|
|
def check_enrollment(ignore_restricted_courses = false)
|
|
return false if @pending_enrollment
|
|
|
|
if enrollment = fetch_enrollment
|
|
if enrollment.state_based_on_date == :inactive && !ignore_restricted_courses
|
|
flash[:notice] = t('notices.enrollment_not_active', 'Your membership in the course, %{course}, is not yet activated', :course => @context.name)
|
|
return !!redirect_to(enrollment.workflow_state == 'invited' ? courses_url : dashboard_url)
|
|
end
|
|
|
|
if enrollment.rejected?
|
|
enrollment.workflow_state = 'invited'
|
|
enrollment.save_without_broadcasting
|
|
end
|
|
|
|
if enrollment.self_enrolled?
|
|
return !!redirect_to(registration_confirmation_path(enrollment.user.email_channel.confirmation_code, :enrollment => enrollment.uuid))
|
|
end
|
|
|
|
session[:enrollment_uuid] = enrollment.uuid
|
|
session[:enrollment_uuid_course_id] = enrollment.course_id
|
|
session[:permissions_key] = CanvasUUID.generate
|
|
|
|
@pending_enrollment = enrollment
|
|
|
|
if @context.root_account.allow_invitation_previews?
|
|
flash[:notice] = t('notices.preview_course', "You've been invited to join this course. You can look around, but you'll need to accept the enrollment invitation before you can participate.")
|
|
elsif params[:action] != "enrollment_invitation"
|
|
# directly call the next action; it's just going to redirect anyway, so no need to have
|
|
# an additional redirect to get to it
|
|
params[:accept] = 1
|
|
return enrollment_invitation
|
|
end
|
|
end
|
|
|
|
if session[:accepted_enrollment_uuid].present? &&
|
|
enrollment = @context.enrollments.where(uuid: session[:accepted_enrollment_uuid]).first
|
|
|
|
success = false
|
|
if enrollment.invited?
|
|
success = enrollment.accept!
|
|
flash[:notice] = message || t('notices.invitation_accepted', "Invitation accepted! Welcome to %{course}!", :course => @context.name)
|
|
end
|
|
|
|
if session[:enrollment_uuid] == session[:accepted_enrollment_uuid]
|
|
session.delete(:enrollment_uuid)
|
|
session[:permissions_key] = CanvasUUID.generate
|
|
end
|
|
session.delete(:accepted_enrollment_uuid)
|
|
session.delete(:enrollment_uuid_course_id)
|
|
end
|
|
|
|
false
|
|
end
|
|
protected :check_enrollment
|
|
|
|
# Internal: Get the current user's enrollment (if any) in the requested course.
|
|
#
|
|
# Returns enrollment (or nil).
|
|
def fetch_enrollment
|
|
# Use the enrollment we already fetched, if possible
|
|
enrollment = @context_enrollment if @context_enrollment && @context_enrollment.pending? && (@context_enrollment.uuid == params[:invitation] || params[:invitation].blank?)
|
|
|
|
# Overwrite with the session enrollment, if one exists, and it's different than the current user's
|
|
if session[:enrollment_uuid] && enrollment.try(:uuid) != session[:enrollment_uuid] &&
|
|
params[:invitation].blank? && session[:enrollment_uuid_course_id] == @context.id
|
|
|
|
enrollment = @context.enrollments.where(uuid: session[:enrollment_uuid], workflow_state: "invited").first
|
|
end
|
|
|
|
# Look for enrollments to matching temporary users
|
|
if @current_user
|
|
enrollment ||= @current_user.temporary_invitations.find do |invitation|
|
|
invitation.course_id == @context.id
|
|
end
|
|
end
|
|
|
|
# Look up the explicitly provided invitation
|
|
unless params[:invitation].blank?
|
|
enrollment ||= @context.enrollments.where("enrollments.uuid=? AND enrollments.workflow_state IN ('invited', 'rejected')", params[:invitation]).first
|
|
end
|
|
|
|
enrollment
|
|
end
|
|
protected :fetch_enrollment
|
|
|
|
def locks
|
|
if authorized_action(@context, @current_user, :read)
|
|
assets = params[:assets].split(",")
|
|
types = {}
|
|
assets.each do |asset|
|
|
split = asset.split("_")
|
|
id = split.pop
|
|
(types[split.join("_")] ||= []) << id
|
|
end
|
|
locks_hash = Rails.cache.fetch(['locked_for_results', @current_user, Digest::MD5.hexdigest(params[:assets])].cache_key) do
|
|
locks = {}
|
|
types.each do |type, ids|
|
|
if type == 'assignment'
|
|
@context.assignments.active.where(id: ids).each do |assignment|
|
|
locks[assignment.asset_string] = assignment.locked_for?(@current_user)
|
|
end
|
|
elsif type == 'quiz'
|
|
@context.quizzes.active.include_assignment.where(id: ids).each do |quiz|
|
|
locks[quiz.asset_string] = quiz.locked_for?(@current_user)
|
|
end
|
|
elsif type == 'discussion_topic'
|
|
@context.discussion_topics.active.where(id: ids).each do |topic|
|
|
locks[topic.asset_string] = topic.locked_for?(@current_user)
|
|
end
|
|
end
|
|
end
|
|
locks
|
|
end
|
|
render :json => locks_hash
|
|
end
|
|
end
|
|
|
|
def self_unenrollment
|
|
get_context
|
|
if @context_enrollment && params[:self_unenrollment] && params[:self_unenrollment] == @context_enrollment.uuid && @context_enrollment.self_enrolled?
|
|
@context_enrollment.conclude
|
|
render :json => ""
|
|
else
|
|
render :json => "", :status => :bad_request
|
|
end
|
|
end
|
|
|
|
# DEPRECATED
|
|
def self_enrollment
|
|
get_context
|
|
unless params[:self_enrollment] &&
|
|
@context.self_enrollment_codes.include?(params[:self_enrollment]) &&
|
|
@context.self_enrollment_code
|
|
return redirect_to course_url(@context)
|
|
end
|
|
redirect_to enroll_url(@context.self_enrollment_code)
|
|
end
|
|
|
|
def check_pending_teacher
|
|
store_location if @context.created?
|
|
if session[:saved_course_uuid] == @context.uuid
|
|
@context_just_saved = true
|
|
session.delete(:saved_course_uuid)
|
|
end
|
|
return unless session[:claimed_course_uuids] && session[:claimed_enrollment_uuids]
|
|
if session[:claimed_course_uuids].include?(@context.uuid)
|
|
session[:claimed_enrollment_uuids].each do |uuid|
|
|
e = @context.enrollments.where(uuid: uuid).first
|
|
@pending_teacher = e.user if e
|
|
end
|
|
end
|
|
end
|
|
protected :check_pending_teacher
|
|
|
|
def check_unknown_user
|
|
@public_view = true unless @current_user && @context.grants_right?(@current_user, session, :read_roster)
|
|
end
|
|
protected :check_unknown_user
|
|
|
|
def check_for_xlist
|
|
return false unless @current_user.present? && @context_enrollment.blank?
|
|
xlist_enrollment = @current_user.enrollments.active.joins(:course_section).
|
|
where(:course_sections => { :nonxlist_course_id => @context }).first
|
|
if xlist_enrollment.present?
|
|
redirect_params = {}
|
|
redirect_params[:invitation] = params[:invitation] if params[:invitation].present?
|
|
redirect_to course_path(xlist_enrollment.course_id, redirect_params)
|
|
return true
|
|
end
|
|
false
|
|
end
|
|
protected :check_for_xlist
|
|
|
|
include ContextModulesController::ModuleIndexHelper
|
|
|
|
# @API Get a single course
|
|
# Return information on a single course.
|
|
#
|
|
# Accepts the same include[] parameters as the list action plus:
|
|
#
|
|
# @argument include[] [String, "all_courses"|"permissions"]
|
|
# - "all_courses": Also search recently deleted courses.
|
|
# - "permissions": Include permissions the current user has
|
|
# for the course.
|
|
#
|
|
# @returns Course
|
|
def show
|
|
if api_request?
|
|
includes = Set.new(Array(params[:include]))
|
|
|
|
if params[:account_id]
|
|
@account = api_find(Account.active, params[:account_id])
|
|
scope = @account.root_account? ? @account.all_courses : @account.associated_courses
|
|
else
|
|
scope = Course
|
|
end
|
|
|
|
if !includes.member?("all_courses")
|
|
scope = scope.not_deleted
|
|
end
|
|
@course = api_find(scope, params[:id])
|
|
|
|
if authorized_action(@course, @current_user, :read)
|
|
enrollments = @course.current_enrollments.where(:user_id => @current_user).all
|
|
includes << :hide_final_grades
|
|
render :json => course_json(@course, @current_user, session, includes, enrollments)
|
|
end
|
|
return
|
|
end
|
|
|
|
|
|
@context = api_find(Course.active, params[:id])
|
|
assign_localizer
|
|
if request.xhr?
|
|
if authorized_action(@context, @current_user, [:read, :read_as_admin])
|
|
render :json => @context
|
|
end
|
|
return
|
|
end
|
|
|
|
if @context && @current_user
|
|
@context_enrollment = @context.enrollments.where(user_id: @current_user).except(:includes).first
|
|
if @context_enrollment
|
|
@context_enrollment.course = @context
|
|
@context_enrollment.user = @current_user
|
|
end
|
|
end
|
|
|
|
return if check_for_xlist
|
|
@unauthorized_message = t('unauthorized.invalid_link', "The enrollment link you used appears to no longer be valid. Please contact the course instructor and make sure you're still correctly enrolled.") if params[:invitation]
|
|
claim_course if session[:claim_course_uuid] || params[:verification]
|
|
@context.claim if @context.created?
|
|
return if check_enrollment
|
|
check_pending_teacher
|
|
check_unknown_user
|
|
@user_groups = @current_user.group_memberships_for(@context) if @current_user
|
|
|
|
if !@context.grants_right?(@current_user, session, :read) && @context.grants_right?(@current_user, session, :read_as_admin)
|
|
return redirect_to course_settings_path(@context.id)
|
|
end
|
|
|
|
@context_enrollment ||= @pending_enrollment
|
|
if is_authorized_action?(@context, @current_user, :read)
|
|
log_asset_access("home:#{@context.asset_string}", "home", "other")
|
|
|
|
check_incomplete_registration
|
|
|
|
add_crumb(@context.short_name, url_for(@context), :id => "crumb_#{@context.asset_string}")
|
|
set_badge_counts_for(@context, @current_user, @current_enrollment)
|
|
|
|
@course_home_view = (params[:view] == "feed" && 'feed') || @context.default_view || 'feed'
|
|
|
|
# make sure the wiki front page exists
|
|
if @course_home_view == 'wiki'
|
|
@context.wiki.check_has_front_page
|
|
@course_home_view = 'feed' if @context.wiki.front_page.nil?
|
|
end
|
|
|
|
@contexts = [@context]
|
|
case @course_home_view
|
|
when "wiki"
|
|
@wiki = @context.wiki
|
|
@page = @wiki.front_page
|
|
set_js_rights [:wiki, :page]
|
|
set_js_wiki_data :course_home => true
|
|
@padless = true
|
|
when 'assignments'
|
|
add_crumb(t('#crumbs.assignments', "Assignments"))
|
|
set_urls_and_permissions_for_assignment_index
|
|
get_sorted_assignments
|
|
when 'modules'
|
|
add_crumb(t('#crumbs.modules', "Modules"))
|
|
load_modules
|
|
when 'syllabus'
|
|
add_crumb(t('#crumbs.syllabus', "Syllabus"))
|
|
@syllabus_body = api_user_content(@context.syllabus_body, @context)
|
|
@groups = @context.assignment_groups.active.order(:position, AssignmentGroup.best_unicode_collation_key('name')).all
|
|
@events = @context.calendar_events.active.to_a
|
|
@events.concat @context.assignments.active.to_a
|
|
@undated_events = @events.select {|e| e.start_at == nil}
|
|
@dates = (@events.select {|e| e.start_at != nil}).map {|e| e.start_at.to_date}.uniq.sort.sort
|
|
else
|
|
@active_tab = "home"
|
|
if @context.grants_right?(@current_user, session, :manage_groups)
|
|
@contexts += @context.groups
|
|
else
|
|
@contexts += @user_groups if @user_groups
|
|
end
|
|
@current_conferences = @context.web_conferences.select{|c| c.active? && c.users.include?(@current_user) }
|
|
@scheduled_conferences = @context.web_conferences.select{|c| c.scheduled? && c.users.include?(@current_user)}
|
|
@stream_items = @current_user.try(:cached_recent_stream_items, { :contexts => @contexts }) || []
|
|
end
|
|
|
|
if @current_user and (@show_recent_feedback = @context.user_is_student?(@current_user))
|
|
@recent_feedback = (@current_user && @current_user.recent_feedback(:contexts => @contexts)) || []
|
|
end
|
|
|
|
@course_home_sub_navigation_tools = ContextExternalTool.all_tools_for(@context, :type => :course_home_sub_navigation, :root_account => @domain_root_account, :current_user => @current_user)
|
|
unless @context.grants_right?(@current_user, session, :manage_content)
|
|
@course_home_sub_navigation_tools.reject! { |tool| tool.course_home_sub_navigation(:visibility) == 'admins' }
|
|
end
|
|
elsif @context.indexed
|
|
render :template => "courses/description"
|
|
else
|
|
# clear notices that would have been displayed as a result of processing
|
|
# an enrollment invitation, since we're giving an error
|
|
flash[:notice] = nil
|
|
render_unauthorized_action
|
|
end
|
|
end
|
|
|
|
def confirm_action
|
|
get_context
|
|
params[:event] ||= (@context.claimed? || @context.created? || @context.completed?) ? 'delete' : 'conclude'
|
|
return unless authorized_action(@context, @current_user, permission_for_event(params[:event]))
|
|
end
|
|
|
|
def conclude_user
|
|
get_context
|
|
@enrollment = @context.enrollments.find(params[:id])
|
|
if @enrollment.can_be_concluded_by(@current_user, @context, session)
|
|
respond_to do |format|
|
|
if @enrollment.conclude
|
|
format.json { render :json => @enrollment }
|
|
else
|
|
format.json { render :json => @enrollment, :status => :bad_request }
|
|
end
|
|
end
|
|
else
|
|
authorized_action(@context, @current_user, :permission_fail)
|
|
end
|
|
end
|
|
|
|
def unconclude_user
|
|
get_context
|
|
@enrollment = @context.enrollments.find(params[:id])
|
|
can_remove = @enrollment.is_a?(StudentEnrollment) && @context.grants_right?(@current_user, session, :manage_students)
|
|
can_remove ||= @context.grants_right?(@current_user, session, :manage_admin_users)
|
|
if can_remove
|
|
respond_to do |format|
|
|
@enrollment.workflow_state = 'active'
|
|
if @enrollment.save
|
|
format.json { render :json => @enrollment }
|
|
else
|
|
format.json { render :json => @enrollment, :status => :bad_request }
|
|
end
|
|
end
|
|
else
|
|
authorized_action(@context, @current_user, :permission_fail)
|
|
end
|
|
end
|
|
|
|
def limit_user
|
|
get_context
|
|
@user = @context.users.find(params[:id])
|
|
if authorized_action(@context, @current_user, :manage_admin_users)
|
|
if params[:limit] == "1"
|
|
Enrollment.limit_privileges_to_course_section!(@context, @user, true)
|
|
render :json => {:limited => true}
|
|
else
|
|
Enrollment.limit_privileges_to_course_section!(@context, @user, false)
|
|
render :json => {:limited => false}
|
|
end
|
|
else
|
|
authorized_action(@context, @current_user, :permission_fail)
|
|
end
|
|
end
|
|
|
|
def unenroll_user
|
|
get_context
|
|
@enrollment = @context.enrollments.find(params[:id])
|
|
if @enrollment.can_be_deleted_by(@current_user, @context, session)
|
|
if (!@enrollment.defined_by_sis? || @context.grants_right?(@current_user, session, :manage_account_settings)) && @enrollment.destroy
|
|
render :json => @enrollment
|
|
else
|
|
render :json => @enrollment, :status => :bad_request
|
|
end
|
|
else
|
|
authorized_action(@context, @current_user, :permission_fail)
|
|
end
|
|
end
|
|
|
|
def enroll_users
|
|
get_context
|
|
params[:enrollment_type] ||= 'StudentEnrollment'
|
|
|
|
custom_role = nil
|
|
if params[:role_id].present? || !Role.get_built_in_role(params[:enrollment_type])
|
|
custom_role = @context.account.get_role_by_id(params[:role_id]) if params[:role_id].present?
|
|
custom_role ||= @context.account.get_role_by_name(params[:enrollment_type]) # backwards compatibility
|
|
if custom_role && custom_role.course_role?
|
|
if custom_role.inactive?
|
|
render :json => t('errors.role_not_active', "Can't add users for non-active role: '%{role}'", :role => custom_role.name), :status => :bad_request
|
|
return
|
|
else
|
|
params[:enrollment_type] = custom_role.base_role_type
|
|
end
|
|
else
|
|
render :json => t('errors.role_not_found', "Could not find role"), :status => :bad_request
|
|
return
|
|
end
|
|
end
|
|
|
|
params[:course_section_id] ||= @context.default_section.id
|
|
if @current_user && @current_user.can_create_enrollment_for?(@context, session, params[:enrollment_type])
|
|
params[:user_list] ||= ""
|
|
|
|
# Enrollment settings hash
|
|
# Change :limit_privileges_to_course_section to be an explicit true/false value
|
|
enrollment_options = params.slice(:course_section_id, :enrollment_type, :limit_privileges_to_course_section)
|
|
limit_privileges = value_to_boolean(enrollment_options[:limit_privileges_to_course_section])
|
|
enrollment_options[:limit_privileges_to_course_section] = limit_privileges
|
|
enrollment_options[:role] = custom_role if custom_role
|
|
list = UserList.new(params[:user_list],
|
|
:root_account => @context.root_account,
|
|
:search_method => @context.user_list_search_mode_for(@current_user),
|
|
:initial_type => params[:enrollment_type])
|
|
if !@context.concluded? && (@enrollments = EnrollmentsFromUserList.process(list, @context, enrollment_options))
|
|
ActiveRecord::Associations::Preloader.new(@enrollments, [:course_section, {:user => [:communication_channel, :pseudonym]}]).run
|
|
json = @enrollments.map { |e|
|
|
{ 'enrollment' =>
|
|
{ 'associated_user_id' => e.associated_user_id,
|
|
'communication_channel_id' => e.user.communication_channel.try(:id),
|
|
'email' => e.email,
|
|
'id' => e.id,
|
|
'name' => (e.user.last_name_first || e.user.name),
|
|
'pseudonym_id' => e.user.pseudonym.try(:id),
|
|
'section' => e.course_section.display_name,
|
|
'short_name' => e.user.short_name,
|
|
'type' => e.type,
|
|
'user_id' => e.user_id,
|
|
'workflow_state' => e.workflow_state,
|
|
'role_id' => e.role_id,
|
|
'already_enrolled' => e.already_enrolled
|
|
}
|
|
}
|
|
}
|
|
render :json => json
|
|
else
|
|
render :json => "", :status => :bad_request
|
|
end
|
|
else
|
|
authorized_action(@context, @current_user, :permission_fail)
|
|
end
|
|
end
|
|
|
|
def link_enrollment
|
|
get_context
|
|
if authorized_action(@context, @current_user, :manage_admin_users)
|
|
enrollment = @context.observer_enrollments.find(params[:enrollment_id])
|
|
student = nil
|
|
student = @context.students.find(params[:student_id]) if params[:student_id] != 'none'
|
|
enrollment.update_attribute(:associated_user_id, student && student.id)
|
|
render :json => enrollment.as_json(:methods => :associated_user_name)
|
|
end
|
|
end
|
|
|
|
def move_enrollment
|
|
get_context
|
|
@enrollment = @context.enrollments.find(params[:id])
|
|
can_move = [StudentEnrollment, ObserverEnrollment].include?(@enrollment.class) && @context.grants_right?(@current_user, session, :manage_students)
|
|
can_move ||= @context.grants_right?(@current_user, session, :manage_admin_users)
|
|
can_move &&= @context.grants_right?(@current_user, session, :manage_account_settings) if @enrollment.defined_by_sis?
|
|
if can_move
|
|
respond_to do |format|
|
|
# ensure user_id,section_id,type,associated_user_id is unique (this
|
|
# will become a DB constraint eventually)
|
|
@possible_dup = @context.enrollments.where(
|
|
"id<>? AND user_id=? AND course_section_id=? AND type=? AND (associated_user_id IS NULL OR associated_user_id=?)",
|
|
@enrollment, @enrollment.user_id, params[:course_section_id], @enrollment.type, @enrollment.associated_user_id).first
|
|
if @possible_dup.present?
|
|
format.json { render :json => @enrollment, :status => :forbidden }
|
|
else
|
|
@enrollment.course_section = @context.course_sections.find(params[:course_section_id])
|
|
@enrollment.save!
|
|
|
|
format.json { render :json => @enrollment }
|
|
end
|
|
end
|
|
else
|
|
authorized_action(@context, @current_user, :permission_fail)
|
|
end
|
|
end
|
|
|
|
def copy
|
|
get_context
|
|
authorized_action(@context, @current_user, :read) &&
|
|
authorized_action(@context, @current_user, :read_as_admin) &&
|
|
authorized_action(@domain_root_account.manually_created_courses_account, @current_user, [:create_courses, :manage_courses])
|
|
# For prepopulating the date fields
|
|
js_env(:OLD_START_DATE => unlocalized_datetime_string(@context.start_at, :verbose))
|
|
js_env(:OLD_END_DATE => unlocalized_datetime_string(@context.conclude_at, :verbose))
|
|
end
|
|
|
|
def copy_course
|
|
get_context
|
|
if authorized_action(@context, @current_user, :read) &&
|
|
authorized_action(@context, @current_user, :read_as_admin)
|
|
args = params[:course].slice(:name, :course_code)
|
|
account = @context.account
|
|
if params[:course][:account_id]
|
|
account = Account.find(params[:course][:account_id])
|
|
end
|
|
account = nil unless account.grants_any_right?(@current_user, session, :create_courses, :manage_courses)
|
|
account ||= @domain_root_account.manually_created_courses_account
|
|
return unless authorized_action(account, @current_user, [:create_courses, :manage_courses])
|
|
if account.grants_right?(@current_user, session, :manage_courses)
|
|
root_account = account.root_account
|
|
enrollment_term_id = params[:course].delete(:term_id).presence || params[:course].delete(:enrollment_term_id).presence
|
|
args[:enrollment_term] = root_account.enrollment_terms.where(id: enrollment_term_id).first if enrollment_term_id
|
|
end
|
|
args[:enrollment_term] ||= @context.enrollment_term
|
|
args[:abstract_course] = @context.abstract_course
|
|
args[:account] = account
|
|
@course = @context.account.courses.new
|
|
@course.attributes = args
|
|
@course.start_at = DateTime.parse(params[:course][:start_at]).utc rescue nil
|
|
@course.conclude_at = DateTime.parse(params[:course][:conclude_at]).utc rescue nil
|
|
@course.workflow_state = 'claimed'
|
|
@course.save
|
|
@course.enroll_user(@current_user, 'TeacherEnrollment', :enrollment_state => 'active')
|
|
|
|
@content_migration = @course.content_migrations.build(:user => @current_user, :source_course => @context, :context => @course, :migration_type => 'course_copy_importer', :initiated_source => api_request? ? :api : :manual)
|
|
@content_migration.migration_settings[:source_course_id] = @context.id
|
|
@content_migration.workflow_state = 'created'
|
|
if (adjust_dates = params[:adjust_dates]) && Canvas::Plugin.value_to_boolean(adjust_dates[:enabled])
|
|
params[:date_shift_options][adjust_dates[:operation]] = '1'
|
|
end
|
|
@content_migration.set_date_shift_options(params[:date_shift_options])
|
|
|
|
if Canvas::Plugin.value_to_boolean(params[:selective_import])
|
|
@content_migration.migration_settings[:import_immediately] = false
|
|
@content_migration.workflow_state = 'exported'
|
|
@content_migration.save
|
|
else
|
|
@content_migration.migration_settings[:import_immediately] = true
|
|
@content_migration.copy_options = {:everything => true}
|
|
@content_migration.migration_settings[:migration_ids_to_import] = {:copy => {:everything => true}}
|
|
@content_migration.workflow_state = 'importing'
|
|
@content_migration.save
|
|
@content_migration.queue_migration
|
|
end
|
|
|
|
redirect_to course_content_migrations_url(@course)
|
|
end
|
|
end
|
|
|
|
# @API Update a course
|
|
# Update an existing course.
|
|
#
|
|
# Arguments are the same as Courses#create, with a few exceptions (enroll_me).
|
|
#
|
|
# @argument account_id [Required, Integer]
|
|
# The unique ID of the account to create to course under.
|
|
#
|
|
# @argument course[name] [String]
|
|
# The name of the course. If omitted, the course will be named "Unnamed
|
|
# Course."
|
|
#
|
|
# @argument course[course_code] [String]
|
|
# The course code for the course.
|
|
#
|
|
# @argument course[start_at] [DateTime]
|
|
# Course start date in ISO8601 format, e.g. 2011-01-01T01:00Z
|
|
#
|
|
# @argument course[end_at] [DateTime]
|
|
# Course end date in ISO8601 format. e.g. 2011-01-01T01:00Z
|
|
#
|
|
# @argument course[license] [String]
|
|
# The name of the licensing. Should be one of the following abbreviations
|
|
# (a descriptive name is included in parenthesis for reference):
|
|
# - 'private' (Private Copyrighted)
|
|
# - 'cc_by_nc_nd' (CC Attribution Non-Commercial No Derivatives)
|
|
# - 'cc_by_nc_sa' (CC Attribution Non-Commercial Share Alike)
|
|
# - 'cc_by_nc' (CC Attribution Non-Commercial)
|
|
# - 'cc_by_nd' (CC Attribution No Derivatives)
|
|
# - 'cc_by_sa' (CC Attribution Share Alike)
|
|
# - 'cc_by' (CC Attribution)
|
|
# - 'public_domain' (Public Domain).
|
|
#
|
|
# @argument course[is_public] [Boolean]
|
|
# Set to true if course if public.
|
|
#
|
|
# @argument course[public_syllabus] [Boolean]
|
|
# Set to true to make the course syllabus public.
|
|
#
|
|
# @argument course[public_description] [String]
|
|
# A publicly visible description of the course.
|
|
#
|
|
# @argument course[allow_student_wiki_edits] [Boolean]
|
|
# If true, students will be able to modify the course wiki.
|
|
#
|
|
# @argument course[allow_wiki_comments] [Boolean]
|
|
# If true, course members will be able to comment on wiki pages.
|
|
#
|
|
# @argument course[allow_student_forum_attachments] [Boolean]
|
|
# If true, students can attach files to forum posts.
|
|
#
|
|
# @argument course[open_enrollment] [Boolean]
|
|
# Set to true if the course is open enrollment.
|
|
#
|
|
# @argument course[self_enrollment] [Boolean]
|
|
# Set to true if the course is self enrollment.
|
|
#
|
|
# @argument course[restrict_enrollments_to_course_dates] [Boolean]
|
|
# Set to true to restrict user enrollments to the start and end dates of the
|
|
# course.
|
|
#
|
|
# @argument course[term_id] [Integer]
|
|
# The unique ID of the term to create to course in.
|
|
#
|
|
# @argument course[sis_course_id] [String]
|
|
# The unique SIS identifier.
|
|
#
|
|
# @argument course[integration_id] [String]
|
|
# The unique Integration identifier.
|
|
#
|
|
# @argument course[hide_final_grades] [Boolean]
|
|
# If this option is set to true, the totals in student grades summary will
|
|
# be hidden.
|
|
#
|
|
# @argument course[apply_assignment_group_weights] [Boolean]
|
|
# Set to true to weight final grade based on assignment groups percentages.
|
|
#
|
|
# @argument offer [Boolean]
|
|
# If this option is set to true, the course will be available to students
|
|
# immediately.
|
|
#
|
|
# @argument course[syllabus_body] [String]
|
|
# The syllabus body for the course
|
|
#
|
|
# @argument course[grading_standard_id] [Integer]
|
|
# The grading standard id to set for the course. If no value is provided for this argument the current grading_standard will be un-set from this course.
|
|
#
|
|
# @argument course[course_format] [String]
|
|
# Optional. Specifies the format of the course. (Should be either 'on_campus' or 'online')
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id> \
|
|
# -X PUT \
|
|
# -H 'Authorization: Bearer <token>' \
|
|
# -d 'course[name]=New course name' \
|
|
# -d 'course[start_at]=2012-05-05T00:00:00Z'
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "name": "New course name",
|
|
# "course_code": "COURSE-001",
|
|
# "start_at": "2012-05-05T00:00:00Z",
|
|
# "end_at": "2012-08-05T23:59:59Z",
|
|
# "sis_course_id": "12345"
|
|
# }
|
|
def update
|
|
@course = api_find(Course, params[:id])
|
|
old_settings = @course.settings
|
|
logging_source = api_request? ? :api : :manual
|
|
|
|
if authorized_action(@course, @current_user, :update)
|
|
params[:course] ||= {}
|
|
if params[:course].has_key?(:syllabus_body)
|
|
params[:course][:syllabus_body] = process_incoming_html_content(params[:course][:syllabus_body])
|
|
end
|
|
|
|
account_id = params[:course].delete :account_id
|
|
if account_id && @course.account.grants_right?(@current_user, session, :manage_courses)
|
|
account = api_find(Account, account_id)
|
|
if account && account != @course.account && account.grants_right?(@current_user, session, :manage_courses)
|
|
@course.account = account
|
|
end
|
|
end
|
|
|
|
root_account_id = params[:course].delete :root_account_id
|
|
if root_account_id && Account.site_admin.grants_right?(@current_user, session, :manage_courses)
|
|
@course.root_account = Account.root_accounts.find(root_account_id)
|
|
@course.account = @course.root_account if @course.account.root_account != @course.root_account
|
|
end
|
|
|
|
term_id = params[:course].delete(:term_id)
|
|
enrollment_term_id = params[:course].delete(:enrollment_term_id) || term_id
|
|
if enrollment_term_id && @course.root_account.grants_right?(@current_user, session, :manage_courses)
|
|
enrollment_term = api_find(@course.root_account.enrollment_terms, enrollment_term_id)
|
|
@course.enrollment_term = enrollment_term if enrollment_term && enrollment_term != @course.enrollment_term
|
|
end
|
|
|
|
if params[:course].has_key? :grading_standard_id
|
|
standard_id = params[:course].delete :grading_standard_id
|
|
if @course.grants_right?(@current_user, session, :manage_grades)
|
|
if standard_id.present?
|
|
grading_standard = GradingStandard.standards_for(@course).where(id: standard_id).first
|
|
@course.grading_standard = grading_standard if grading_standard
|
|
else
|
|
@course.grading_standard = nil
|
|
end
|
|
end
|
|
end
|
|
unless @course.account.grants_right? @current_user, session, :manage_storage_quotas
|
|
params[:course].delete :storage_quota
|
|
params[:course].delete :storage_quota_mb
|
|
end
|
|
if !@course.account.grants_right?(@current_user, session, :manage_courses)
|
|
if @course.root_account.settings[:prevent_course_renaming_by_teachers]
|
|
params[:course].delete :name
|
|
params[:course].delete :course_code
|
|
end
|
|
end
|
|
params[:course][:sis_source_id] = params[:course].delete(:sis_course_id) if api_request?
|
|
if sis_id = params[:course].delete(:sis_source_id)
|
|
if sis_id != @course.sis_source_id && @course.root_account.grants_right?(@current_user, session, :manage_sis)
|
|
if sis_id == ''
|
|
@course.sis_source_id = nil
|
|
else
|
|
@course.sis_source_id = sis_id
|
|
end
|
|
end
|
|
end
|
|
if params[:course].has_key?(:apply_assignment_group_weights)
|
|
@course.apply_assignment_group_weights = value_to_boolean params[:course].delete(:apply_assignment_group_weights)
|
|
end
|
|
params[:course][:event] = :offer if params[:offer].present?
|
|
|
|
lock_announcements = params[:course].delete(:lock_all_announcements)
|
|
if value_to_boolean(lock_announcements)
|
|
@course.lock_all_announcements = true
|
|
Announcement.where(:context_type => 'Course', :context_id => @course, :workflow_state => 'active').
|
|
update_all(:locked => true)
|
|
elsif @course.lock_all_announcements
|
|
@course.lock_all_announcements = false
|
|
end
|
|
|
|
if params[:course].has_key?(:locale) && params[:course][:locale].blank?
|
|
params[:course][:locale] = nil
|
|
end
|
|
|
|
if params[:course][:event] && @course.grants_right?(@current_user, session, :change_course_state)
|
|
event = params[:course].delete(:event)
|
|
event = event.to_sym
|
|
if event == :claim && !@course.unpublishable?
|
|
flash[:error] = t('errors.unpublish', 'Course cannot be unpublished if student submissions exist.')
|
|
redirect_to(course_url(@course)) and return
|
|
else
|
|
@course.process_event(event)
|
|
if event == :offer
|
|
Auditors::Course.record_published(@course, @current_user, source: logging_source)
|
|
elsif event == :claim
|
|
Auditors::Course.record_claimed(@course, @current_user, source: logging_source)
|
|
end
|
|
end
|
|
end
|
|
|
|
params[:course][:conclude_at] = params[:course].delete(:end_at) if api_request? && params[:course].has_key?(:end_at)
|
|
respond_to do |format|
|
|
@default_wiki_editing_roles_was = @course.default_wiki_editing_roles
|
|
|
|
@course.attributes = params[:course]
|
|
changes = changed_settings(@course.changes, @course.settings, old_settings)
|
|
|
|
if params[:for_reload] || @course.save
|
|
Auditors::Course.record_updated(@course, @current_user, changes, source: logging_source)
|
|
@current_user.touch
|
|
if params[:update_default_pages]
|
|
@course.wiki.update_default_wiki_page_roles(@course.default_wiki_editing_roles, @default_wiki_editing_roles_was)
|
|
end
|
|
format.html {
|
|
flash[:notice] = t('notices.updated', 'Course was successfully updated.')
|
|
redirect_to((!params[:continue_to] || params[:continue_to].empty?) ? course_url(@course) : params[:continue_to])
|
|
}
|
|
format.json do
|
|
if api_request?
|
|
render :json => course_json(@course, @current_user, session, [:hide_final_grades], nil)
|
|
else
|
|
render :json => @course.as_json(:methods => [:readable_license, :quota, :account_name, :term_name, :grading_standard_title, :storage_quota_mb]), :status => :ok
|
|
end
|
|
end
|
|
else
|
|
format.html { render :action => "edit" }
|
|
format.json { render :json => @course.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Update courses
|
|
# Update multiple courses in an account. Operates asynchronously; use the {api:ProgressController#show progress endpoint}
|
|
# to query the status of an operation.
|
|
#
|
|
# @argument course_ids[] [Required] List of ids of courses to update. At most 500 courses may be updated in one call.
|
|
# @argument event [Required, String, "offer"|"conclude"|"delete"|"undelete"]
|
|
# The action to take on each course. Must be one of 'offer', 'conclude', 'delete', or 'undelete'.
|
|
# * 'offer' makes a course visible to students. This action is also called "publish" on the web site.
|
|
# * 'conclude' prevents future enrollments and makes a course read-only for all participants. The course still appears
|
|
# in prior-enrollment lists.
|
|
# * 'delete' completely removes the course from the web site (including course menus and prior-enrollment lists).
|
|
# All enrollments are deleted. Course content may be physically deleted at a future date.
|
|
# * 'undelete' attempts to recover a course that has been deleted. (Recovery is not guaranteed; please conclude
|
|
# rather than delete a course if there is any possibility the course will be used again.) The recovered course
|
|
# will be unpublished. Deleted enrollments will not be recovered.
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/accounts/<account_id>/courses \
|
|
# -X PUT \
|
|
# -H 'Authorization: Bearer <token>' \
|
|
# -d 'event=offer' \
|
|
# -d 'course_ids[]=1' \
|
|
# -d 'course_ids[]=2'
|
|
#
|
|
# @returns Progress
|
|
def batch_update
|
|
@account = api_find(Account, params[:account_id])
|
|
if params[:event] == 'undelete'
|
|
permission = :undelete_courses
|
|
else
|
|
permission = :manage_courses
|
|
end
|
|
|
|
if authorized_action(@account, @current_user, permission)
|
|
return render(:json => { :message => 'must specify course_ids[]' }, :status => :bad_request) unless params[:course_ids].is_a?(Array)
|
|
@course_ids = Api.map_ids(params[:course_ids], Course, @domain_root_account, @current_user)
|
|
return render(:json => { :message => 'course batch size limit (500) exceeded' }, :status => :forbidden) if @course_ids.size > 500
|
|
update_params = params.slice(:event).with_indifferent_access
|
|
return render(:json => { :message => 'need to specify event' }, :status => :bad_request) unless update_params[:event]
|
|
return render(:json => { :message => 'invalid event' }, :status => :bad_request) unless %w(offer conclude delete undelete).include? update_params[:event]
|
|
progress = Course.batch_update(@account, @current_user, @course_ids, update_params, :api)
|
|
render :json => progress_json(progress, @current_user, session)
|
|
end
|
|
end
|
|
|
|
def public_feed
|
|
return unless get_feed_context(:only => [:course])
|
|
feed = Atom::Feed.new do |f|
|
|
f.title = t('titles.rss_feed', "%{course} Feed", :course => @context.name)
|
|
f.links << Atom::Link.new(:href => course_url(@context), :rel => 'self')
|
|
f.updated = Time.now
|
|
f.id = course_url(@context)
|
|
end
|
|
@entries = []
|
|
@entries.concat @context.assignments.published
|
|
@entries.concat @context.calendar_events.active
|
|
@entries.concat(@context.discussion_topics.active.select{ |dt|
|
|
dt.published? && !dt.locked_for?(@current_user, :check_policies => true)
|
|
})
|
|
@entries.concat @context.wiki.wiki_pages
|
|
@entries = @entries.sort_by{|e| e.updated_at}
|
|
@entries.each do |entry|
|
|
feed.entries << entry.to_atom(:context => @context)
|
|
end
|
|
respond_to do |format|
|
|
format.atom { render :text => feed.to_xml }
|
|
end
|
|
end
|
|
|
|
def publish_to_sis
|
|
sis_publish_status(true)
|
|
end
|
|
|
|
def sis_publish_status(publish_grades=false)
|
|
get_context
|
|
return unless authorized_action(@context, @current_user, :manage_grades)
|
|
@context.publish_final_grades(@current_user) if publish_grades
|
|
|
|
processed_grade_publishing_statuses = {}
|
|
grade_publishing_statuses, overall_status = @context.grade_publishing_statuses
|
|
grade_publishing_statuses.each do |message, enrollments|
|
|
processed_grade_publishing_statuses[message] = enrollments.map do |enrollment|
|
|
{ :id => enrollment.user.id,
|
|
:name => enrollment.user.name,
|
|
:sortable_name => enrollment.user.sortable_name,
|
|
:url => course_user_url(@context, enrollment.user) }
|
|
end
|
|
end
|
|
|
|
render :json => { :sis_publish_overall_status => overall_status,
|
|
:sis_publish_statuses => processed_grade_publishing_statuses }
|
|
end
|
|
|
|
# @API Reset a course
|
|
# Deletes the current course, and creates a new equivalent course with
|
|
# no content, but all sections and users moved over.
|
|
#
|
|
# @returns Course
|
|
def reset_content
|
|
get_context
|
|
return unless authorized_action(@context, @current_user, :reset_content)
|
|
@new_course = @context.reset_content
|
|
Auditors::Course.record_reset(@context, @new_course, @current_user, source: api_request? ? :api : :manual)
|
|
if api_request?
|
|
render :json => course_json(@new_course, @current_user, session, [], nil)
|
|
else
|
|
redirect_to course_settings_path(@new_course.id)
|
|
end
|
|
end
|
|
|
|
def student_view
|
|
get_context
|
|
if authorized_action(@context, @current_user, :use_student_view)
|
|
enter_student_view
|
|
end
|
|
end
|
|
|
|
def leave_student_view
|
|
session.delete(:become_user_id)
|
|
return_url = session[:masquerade_return_to]
|
|
session.delete(:masquerade_return_to)
|
|
return return_to(return_url, request.referer || dashboard_url)
|
|
end
|
|
|
|
def reset_test_student
|
|
get_context
|
|
if @current_user.fake_student? && authorized_action(@context, @real_current_user, :use_student_view)
|
|
# destroy the exising student
|
|
@fake_student = @context.student_view_student
|
|
# but first, remove all existing quiz submissions / submissions
|
|
Submission.where(quiz_submission_id: @fake_student.quiz_submissions.select(:id)).destroy_all
|
|
@fake_student.quiz_submissions.destroy_all
|
|
|
|
@fake_student.destroy
|
|
flash[:notice] = t('notices.reset_test_student', "The test student has been reset successfully.")
|
|
enter_student_view
|
|
end
|
|
end
|
|
|
|
def enter_student_view
|
|
@fake_student = @context.student_view_student
|
|
session[:become_user_id] = @fake_student.id
|
|
return_url = course_path(@context)
|
|
session.delete(:masquerade_return_to)
|
|
return return_to(return_url, request.referer || dashboard_url)
|
|
end
|
|
protected :enter_student_view
|
|
|
|
def permission_for_event(event)
|
|
case event
|
|
when 'conclude'
|
|
:change_course_state
|
|
when 'delete'
|
|
:delete
|
|
else
|
|
:nothing
|
|
end
|
|
end
|
|
|
|
def changed_settings(changes, new_settings, old_settings=nil)
|
|
# frd? storing a hash?
|
|
# Settings is stored as a hash in a column which
|
|
# causes us to do some more work if it has been changed.
|
|
|
|
# Since course uses write_attribute on settings its not accurate
|
|
# so just ignore it if its in the changes hash
|
|
changes.delete("settings") if changes.has_key?("settings")
|
|
|
|
unless old_settings == new_settings
|
|
settings = Course.settings_options.keys.inject({}) do |results, key|
|
|
if old_settings.present? && old_settings.has_key?(key)
|
|
old_value = old_settings[key]
|
|
else
|
|
old_value = nil
|
|
end
|
|
|
|
if new_settings.present? && new_settings.has_key?(key)
|
|
new_value = new_settings[key]
|
|
else
|
|
new_value = nil
|
|
end
|
|
|
|
results[key.to_s] = [ old_value, new_value ] unless old_value == new_value
|
|
|
|
results
|
|
end
|
|
changes.merge!(settings)
|
|
end
|
|
|
|
changes
|
|
end
|
|
|
|
def set_urls_and_permissions_for_assignment_index
|
|
permissions = {manage: false}
|
|
js_env({
|
|
:COURSE_HOME => true,
|
|
:URLS => {
|
|
:new_assignment_url => new_polymorphic_url([@context, :assignment]),
|
|
:course_url => api_v1_course_url(@context),
|
|
:context_modules_url => api_v1_course_context_modules_path(@context),
|
|
:course_student_submissions_url => api_v1_course_student_submissions_url(@context)
|
|
},
|
|
:PERMISSIONS => permissions,
|
|
})
|
|
end
|
|
|
|
def ping
|
|
render json: {success: true}
|
|
end
|
|
|
|
def link_validation
|
|
get_context
|
|
return unless authorized_action(@context, @current_user, :manage_content)
|
|
|
|
if progress = CourseLinkValidator.current_progress(@context)
|
|
hash = {:state => progress.workflow_state}
|
|
if !progress.pending? && progress.results
|
|
hash.merge!(progress.results)
|
|
end
|
|
render :json => hash
|
|
else
|
|
render :json => {}
|
|
end
|
|
end
|
|
|
|
def start_link_validation
|
|
get_context
|
|
return unless authorized_action(@context, @current_user, :manage_content)
|
|
|
|
CourseLinkValidator.queue_course(@context)
|
|
render :json => {:success => true}
|
|
end
|
|
end
|