442 lines
16 KiB
Ruby
442 lines
16 KiB
Ruby
#
|
|
# Copyright (C) 2015 - present 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 SIS Integration
|
|
#
|
|
# @model SisAssignment
|
|
# {
|
|
# "id": "SisAssignment",
|
|
# "description": "Assignments that have post_to_sis enabled with other objects for convenience",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The unique identifier for the assignment.",
|
|
# "example": 4,
|
|
# "type": "integer"
|
|
# },
|
|
# "course_id": {
|
|
# "description": "The unique identifier for the course.",
|
|
# "example": 6,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "description": "the name of the assignment",
|
|
# "example": "some assignment",
|
|
# "type": "string"
|
|
# },
|
|
# "created_at": {
|
|
# "description": "The time at which this assignment was originally created",
|
|
# "example": "2012-07-01T23:59:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "due_at": {
|
|
# "description": "the due date for the assignment. returns null if not present. NOTE: If this assignment has assignment overrides, this field will be the due date as it applies to the user requesting information from the API.",
|
|
# "example": "2012-07-01T23:59:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "unlock_at": {
|
|
# "description": "(Optional) Time at which this was/will be unlocked.",
|
|
# "example": "2013-01-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "lock_at": {
|
|
# "description": "(Optional) Time at which this was/will be locked.",
|
|
# "example": "2013-02-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "points_possible": {
|
|
# "description": "The maximum points possible for the assignment",
|
|
# "example": 12,
|
|
# "type": "integer"
|
|
# },
|
|
# "submission_types": {
|
|
# "description": "the types of submissions allowed for this assignment list containing one or more of the following: 'discussion_topic', 'online_quiz', 'on_paper', 'none', 'external_tool', 'online_text_entry', 'online_url', 'online_upload' 'media_recording'",
|
|
# "example": ["online_text_entry"],
|
|
# "type": "array",
|
|
# "items": {"type": "string"},
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "discussion_topic",
|
|
# "online_quiz",
|
|
# "on_paper",
|
|
# "not_graded",
|
|
# "none",
|
|
# "external_tool",
|
|
# "online_text_entry",
|
|
# "online_url",
|
|
# "online_upload",
|
|
# "media_recording"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "integration_id": {
|
|
# "example": "12341234",
|
|
# "type": "string",
|
|
# "description": "Third Party integration id for assignment"
|
|
# },
|
|
# "integration_data": {
|
|
# "example": "other_data",
|
|
# "type": "string",
|
|
# "description": "(optional, Third Party integration data for assignment)"
|
|
# },
|
|
# "include_in_final_grade": {
|
|
# "description": "If false, the assignment will be omitted from the student's final grade",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "assignment_group": {
|
|
# "description": "Includes attributes of a assignment_group for convenience. For more details see Assignments API.",
|
|
# "type": "array",
|
|
# "items": { "$ref": "AssignmentGroupAttributes" }
|
|
# },
|
|
# "sections": {
|
|
# "description": "Includes attributes of a section for convenience. For more details see Sections API.",
|
|
# "type": "array",
|
|
# "items": { "$ref": "SectionAttributes" }
|
|
# },
|
|
# "user_overrides": {
|
|
# "description": "Includes attributes of a user assignment overrides. For more details see Assignments API.",
|
|
# "type": "array",
|
|
# "items": { "$ref": "UserAssignmentOverrideAttributes" }
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model AssignmentGroupAttributes
|
|
# {
|
|
# "id": "AssignmentGroupAttributes",
|
|
# "description": "Some of the attributes of an Assignment Group. See Assignments API for more details",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "the id of the Assignment Group",
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "description": "the name of the Assignment Group",
|
|
# "example": "group2",
|
|
# "type": "string"
|
|
# },
|
|
# "group_weight": {
|
|
# "description": "the weight of the Assignment Group",
|
|
# "example": 20,
|
|
# "type": "integer"
|
|
# },
|
|
# "sis_source_id": {
|
|
# "description": "the sis source id of the Assignment Group",
|
|
# "example": "1234",
|
|
# "type": "string"
|
|
# },
|
|
# "integration_data": {
|
|
# "description": "the integration data of the Assignment Group",
|
|
# "example": {"5678": "0954"},
|
|
# "type": "object"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model SectionAttributes
|
|
# {
|
|
# "id": "SectionAttributes",
|
|
# "description": "Some of the attributes of a section. For more details see Sections API.",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The unique identifier for the section.",
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "description": "The name of the section.",
|
|
# "example": "Section A",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_id": {
|
|
# "description": "The sis id of the section.",
|
|
# "example": "s34643",
|
|
# "type": "string"
|
|
# },
|
|
# "integration_id": {
|
|
# "description": "Optional: The integration ID of the section.",
|
|
# "example": "3452342345",
|
|
# "type": "string"
|
|
# },
|
|
# "origin_course": {
|
|
# "description": "The course to which the section belongs or the course from which the section was cross-listed",
|
|
# "$ref": "CourseAttributes"
|
|
# },
|
|
# "xlist_course": {
|
|
# "description": "Optional: Attributes of the xlist course. Only present when the section has been cross-listed. See Courses API for more details",
|
|
# "$ref": "CourseAttributes"
|
|
# },
|
|
# "override": {
|
|
# "description": "Optional: Attributes of the assignment override that apply to the section. See Assignment API for more details",
|
|
# "$ref": "SectionAssignmentOverrideAttributes"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model CourseAttributes
|
|
# {
|
|
# "id": "CourseAttributes",
|
|
# "description": "Attributes of a course object. See Courses API for more details",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The unique Canvas identifier for the origin course",
|
|
# "example": 7,
|
|
# "type": "integer"
|
|
# },
|
|
# "name": {
|
|
# "description": "The name of the origin course.",
|
|
# "example": "Section A",
|
|
# "type": "string"
|
|
# },
|
|
# "sis_id": {
|
|
# "description": "The sis id of the origin_course.",
|
|
# "example": "c34643",
|
|
# "type": "string"
|
|
# },
|
|
# "integration_id": {
|
|
# "description": "The integration ID of the origin_course.",
|
|
# "example": "I-2",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model SectionAssignmentOverrideAttributes
|
|
# {
|
|
# "id": "SectionAssignmentOverrideAttributes",
|
|
# "description": "Attributes of an assignment override that apply to the section object. See Assignments API for more details",
|
|
# "properties": {
|
|
# "override_title": {
|
|
# "description": "The title for the assignment override",
|
|
# "example": "some section override",
|
|
# "type": "string"
|
|
# },
|
|
# "due_at": {
|
|
# "description": "the due date for the assignment. returns null if not present. NOTE: If this assignment has assignment overrides, this field will be the due date as it applies to the user requesting information from the API.",
|
|
# "example": "2012-07-01T23:59:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "unlock_at": {
|
|
# "description": "(Optional) Time at which this was/will be unlocked.",
|
|
# "example": "2013-01-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "lock_at": {
|
|
# "description": "(Optional) Time at which this was/will be locked.",
|
|
# "example": "2013-02-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model UserAssignmentOverrideAttributes
|
|
# {
|
|
# "id": "UserAssignmentOverrideAttributes",
|
|
# "description": "Attributes of assignment overrides that apply to users. See Assignments API for more details",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The unique Canvas identifier for the assignment override",
|
|
# "example": 218,
|
|
# "type": "integer"
|
|
# },
|
|
# "title": {
|
|
# "description": "The title of the assignment override.",
|
|
# "example": "Override title",
|
|
# "type": "string"
|
|
# },
|
|
# "due_at": {
|
|
# "description": "The time at which this assignment is due",
|
|
# "example": "2013-01-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "unlock_at": {
|
|
# "description": "(Optional) Time at which this was/will be unlocked.",
|
|
# "example": "2013-01-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "lock_at": {
|
|
# "description": "(Optional) Time at which this was/will be locked.",
|
|
# "example": "2013-02-01T00:00:00-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "students": {
|
|
# "description": "Includes attributes of a student for convenience. For more details see Users API.",
|
|
# "type": "array",
|
|
# "items": { "$ref": "StudentAttributes" }
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model StudentAttributes
|
|
# {
|
|
# "id": "StudentAttributes",
|
|
# "description": "Attributes of student. See Users API for more details",
|
|
# "properties": {
|
|
# "user_id": {
|
|
# "description": "The unique Canvas identifier for the user",
|
|
# "example": 511,
|
|
# "type": "integer"
|
|
# },
|
|
# "sis_user_id": {
|
|
# "description": "The SIS ID associated with the user. This field is only included if the user came from a SIS import and has permissions to view SIS information.",
|
|
# "example": "SHEL93921",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
class SisApiController < ApplicationController
|
|
include Api::V1::SisAssignment
|
|
|
|
before_action :require_view_all_grades, only: [:sis_assignments]
|
|
before_action :require_grade_export, only: [:sis_assignments]
|
|
before_action :require_published_course, only: [:sis_assignments]
|
|
|
|
GRADE_EXPORT_NOT_ENABLED_ERROR = {
|
|
code: 'not_enabled',
|
|
error: 'A SIS integration is not configured and the bulk SIS Grade Export feature is not enabled'.freeze
|
|
}.freeze
|
|
|
|
COURSE_NOT_PUBLISHED_ERROR = {
|
|
code: 'unpublished_course',
|
|
error: 'Grade data is not available for non-published courses'.freeze
|
|
}.freeze
|
|
|
|
# @API Retrieve assignments enabled for grade export to SIS
|
|
# @beta
|
|
#
|
|
# Retrieve a list of published assignments flagged as "post_to_sis".
|
|
# See the Assignments API for more details on assignments.
|
|
# Assignment group and section information are included for convenience.
|
|
#
|
|
# Each section includes course information for the origin course and the
|
|
# cross-listed course, if applicable. The `origin_course` is the course to
|
|
# which the section belongs or the course from which the section was
|
|
# cross-listed. Generally, the `origin_course` should be preferred when
|
|
# performing integration work. The `xlist_course` is provided for consistency
|
|
# and is only present when the section has been cross-listed.
|
|
# See Sections API and Courses Api for me details.
|
|
#
|
|
# The `override` is only provided if the Differentiated Assignments course
|
|
# feature is turned on and the assignment has an override for that section.
|
|
# When there is an override for the assignment the override object's
|
|
# keys/values can be merged with the top level assignment object to create a
|
|
# view of the assignment object specific to that section.
|
|
# See Assignments api for more information on assignment overrides.
|
|
#
|
|
# @argument account_id [Integer] The ID of the account to query.
|
|
# @argument course_id [Integer] The ID of the course to query.
|
|
#
|
|
# @argument starts_before [DateTime, Optional] When searching on an account,
|
|
# restricts to courses that start before this date (if they have a start date)
|
|
# @argument ends_after [DateTime, Optional] When searching on an account,
|
|
# restricts to courses that end after this date (if they have an end date)
|
|
# @argument include [String, "student_overrides"] Array of additional
|
|
# information to include.
|
|
#
|
|
# "student_overrides":: returns individual student override information
|
|
#
|
|
def sis_assignments
|
|
includes = {}
|
|
includes[:student_overrides] = include_student_overrides?
|
|
render json: sis_assignments_json(paginated_assignments, includes)
|
|
end
|
|
|
|
private
|
|
|
|
def context
|
|
@context ||=
|
|
if params[:account_id]
|
|
api_find(Account, params[:account_id])
|
|
elsif params[:course_id]
|
|
api_find(Course, params[:course_id])
|
|
else
|
|
fail ActiveRecord::RecordNotFound, 'unknown context type'
|
|
end
|
|
end
|
|
|
|
def published_course_ids
|
|
if context.is_a?(Account)
|
|
course_scope = Course.published.where(account_id: [context.id] + Account.sub_account_ids_recursive(context.id))
|
|
if starts_before = CanvasTime.try_parse(params[:starts_before])
|
|
course_scope = course_scope.where("
|
|
(courses.start_at IS NULL AND enrollment_terms.start_at IS NULL)
|
|
OR courses.start_at < ? OR enrollment_terms.start_at < ?", starts_before, starts_before)
|
|
end
|
|
if ends_after = CanvasTime.try_parse(params[:ends_after])
|
|
course_scope = course_scope.where("
|
|
(courses.conclude_at IS NULL AND enrollment_terms.end_at IS NULL)
|
|
OR courses.conclude_at > ? OR enrollment_terms.end_at > ?", ends_after, ends_after)
|
|
end
|
|
|
|
if starts_before || ends_after
|
|
course_scope = course_scope.joins(:enrollment_term)
|
|
end
|
|
course_scope
|
|
elsif context.is_a?(Course)
|
|
[context.id]
|
|
end
|
|
end
|
|
|
|
def include_student_overrides?
|
|
params[:include].to_a.include?('student_overrides')
|
|
end
|
|
|
|
def published_assignments
|
|
assignments = Assignment.published.
|
|
where(post_to_sis: true).
|
|
where(context_type: 'Course', context_id: published_course_ids).
|
|
preload(:assignment_group).
|
|
preload(context: {active_course_sections: [:nonxlist_course]})
|
|
|
|
if include_student_overrides?
|
|
assignments = assignments.preload(
|
|
active_assignment_overrides: [assignment_override_students: [user: [:pseudonym]]]
|
|
)
|
|
else
|
|
assignments = assignments.preload(:active_assignment_overrides)
|
|
end
|
|
|
|
assignments
|
|
end
|
|
|
|
def paginated_assignments
|
|
Api.paginate(
|
|
published_assignments.order(:context_id, :id),
|
|
self,
|
|
polymorphic_url([:sis, context, :assignments])
|
|
)
|
|
end
|
|
|
|
def sis_grade_export_enabled?
|
|
Assignment.sis_grade_export_enabled?(context)
|
|
end
|
|
|
|
def require_view_all_grades
|
|
authorized_action(context, @current_user, :view_all_grades)
|
|
end
|
|
|
|
def require_grade_export
|
|
render json: GRADE_EXPORT_NOT_ENABLED_ERROR, status: :bad_request unless sis_grade_export_enabled?
|
|
end
|
|
|
|
def require_published_course
|
|
render json: COURSE_NOT_PUBLISHED_ERROR, status: :bad_request if context.is_a?(Course) && !context.published?
|
|
end
|
|
end
|