258 lines
11 KiB
Ruby
258 lines
11 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 Moderated Grading
|
|
# @subtopic Provisional Grades
|
|
# @beta
|
|
#
|
|
# API for manipulating provisional grades
|
|
#
|
|
# Provisional grades are created by using the Submissions API endpoint "Grade or comment on a submission" with `provisional=true`.
|
|
# They can be viewed by using "List assignment submissions", "Get a single submission", or "List gradeable students" with `include[]=provisional_grades`.
|
|
# This API performs other operations on provisional grades for use with the Moderated Grading feature.
|
|
#
|
|
# @model ProvisionalGrade
|
|
# {
|
|
# "id": "ProvisionalGrade",
|
|
# "description": "",
|
|
# "properties": {
|
|
# "provisional_grade_id": {
|
|
# "description": "The identifier for the provisional grade",
|
|
# "example": 23,
|
|
# "type": "integer"
|
|
# },
|
|
# "score": {
|
|
# "description": "The numeric score",
|
|
# "example": 90,
|
|
# "type": "integer"
|
|
# },
|
|
# "grade": {
|
|
# "description": "The grade",
|
|
# "example": "A-",
|
|
# "type": "string"
|
|
# },
|
|
# "grade_matches_current_submission": {
|
|
# "description": "Whether the grade was applied to the most current submission (false if the student resubmitted after grading)",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "graded_at": {
|
|
# "description": "When the grade was given",
|
|
# "example": "2015-11-01T00:03:21-06:00",
|
|
# "type": "datetime"
|
|
# },
|
|
# "final": {
|
|
# "description": "Whether this is the 'final' provisional grade created by the moderator",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "speedgrader_url": {
|
|
# "description": "A link to view this provisional grade in SpeedGrader™",
|
|
# "example": "http://www.example.com/courses/123/gradebook/speed_grader?...",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
class ProvisionalGradesController < ApplicationController
|
|
before_action :require_user
|
|
before_action :load_assignment
|
|
|
|
include Api::V1::Submission
|
|
|
|
# @API Show provisional grade status for a student
|
|
#
|
|
# Tell whether the student's submission needs one or more provisional grades.
|
|
#
|
|
# @argument student_id [Integer]
|
|
# The id of the student to show the status for
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/courses/1/assignments/2/provisional_grades/status?student_id=1'
|
|
#
|
|
# @example_response
|
|
#
|
|
# { "needs_provisional_grade": false }
|
|
#
|
|
def status
|
|
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
unless @assignment.moderated_grading?
|
|
return render :json => { :message => "Assignment does not use moderated grading" }, :status => :bad_request
|
|
end
|
|
if @assignment.grades_published?
|
|
return render :json => { :message => "Assignment grades have already been published" }, :status => :bad_request
|
|
end
|
|
|
|
# in theory we could apply visibility here, but for now we would rather be performant
|
|
# e.g. @assignment.students_with_visibility(@context.students_visible_to(@current_user)).find(params[:student_id])
|
|
student = @context.students.find(params[:student_id])
|
|
json = {:needs_provisional_grade => @assignment.student_needs_provisional_grade?(student)}
|
|
|
|
|
|
if params[:last_updated_at] # check to see if the submission has been updated
|
|
last_updated = Time.zone.parse(params[:last_updated_at]) # this will be nil if there was originally no submission, so it should match a nil submission
|
|
submission = @assignment.submissions.where(:user_id => student).first
|
|
|
|
if ((submission && submission.updated_at).to_i != last_updated.to_i)
|
|
if authorized_action(@context, @current_user, :moderate_grades) # only do the permission check if it has changed
|
|
selection = @assignment.moderated_grading_selections.where(:student_id => student).first
|
|
|
|
json[:provisional_grades] = []
|
|
submission.provisional_grades.order(:id).each do |pg|
|
|
pg_json = provisional_grade_json(pg, submission, @assignment, @current_user, %w(submission_comments rubric_assessment))
|
|
pg_json[:selected] = !!(selection && selection.selected_provisional_grade_id == pg.id)
|
|
pg_json[:readonly] = !pg.final && (pg.scorer_id != @current_user.id)
|
|
if pg.final
|
|
json[:final_provisional_grade] = pg_json
|
|
else
|
|
json[:provisional_grades] << pg_json
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
render :json => json
|
|
end
|
|
end
|
|
|
|
# @API Select provisional grade
|
|
#
|
|
# Choose which provisional grade the student should receive for a submission.
|
|
# The caller must have :moderate_grades rights.
|
|
#
|
|
# @example_response
|
|
# {
|
|
# "assignment_id": 867,
|
|
# "student_id": 5309,
|
|
# "selected_provisional_grade_id": 53669
|
|
# }
|
|
#
|
|
def select
|
|
if authorized_action(@context, @current_user, :moderate_grades)
|
|
pg = @assignment.provisional_grades.find(params[:provisional_grade_id])
|
|
selection = @assignment.moderated_grading_selections.where(student_id: pg.submission.user_id).first
|
|
return render :json => { :message => 'student not in moderation set' }, :status => :bad_request unless selection
|
|
selection.provisional_grade = pg
|
|
selection.save!
|
|
pg.submission.touch # so the selection is reloaded for other moderators
|
|
render :json => selection.as_json(:include_root => false, :only => %w(assignment_id student_id selected_provisional_grade_id))
|
|
end
|
|
end
|
|
|
|
# @API Copy provisional grade
|
|
#
|
|
# Given a provisional grade, copy the grade (and associated submission comments and rubric assessments)
|
|
# to a "final" mark which can be edited or commented upon by a moderator prior to publication of grades.
|
|
#
|
|
# Notes:
|
|
# * The student must be in the moderation set for the assignment.
|
|
# * The newly created grade will be selected.
|
|
# * The caller must have "Moderate Grades" rights in the course.
|
|
#
|
|
# @returns ProvisionalGrade
|
|
def copy_to_final_mark
|
|
if authorized_action @context, @current_user, :moderate_grades
|
|
pg = @assignment.provisional_grades.find(params[:provisional_grade_id])
|
|
return render :json => { :message => 'provisional grade is already final' }, :status => :bad_request if pg.final
|
|
selection = @assignment.moderated_grading_selections.where(student_id: pg.submission.user_id).first
|
|
return render :json => { :message => 'student not in moderation set' }, :status => :bad_request unless selection
|
|
final_mark = pg.copy_to_final_mark!(@current_user)
|
|
selection.provisional_grade = final_mark
|
|
selection.save!
|
|
render :json => provisional_grade_json(final_mark, pg.submission, @assignment, @current_user, %w(submission_comments rubric_assessment crocodoc_urls)).merge(:selected => true)
|
|
end
|
|
end
|
|
|
|
# @API Publish provisional grades for an assignment
|
|
#
|
|
# Publish the selected provisional grade for all submissions to an assignment.
|
|
# Use the "Select provisional grade" endpoint to choose which provisional grade to publish
|
|
# for a particular submission.
|
|
#
|
|
# Students not in the moderation set will have their one and only provisional grade published.
|
|
#
|
|
# WARNING: This is irreversible. This will overwrite existing grades in the gradebook.
|
|
#
|
|
# @example_request
|
|
#
|
|
# curl 'https://<canvas>/api/v1/courses/1/assignments/2/provisional_grades/publish' \
|
|
# -X POST
|
|
#
|
|
def publish
|
|
if !@context.grants_all_rights?(@current_user, :moderate_grades, :manage_grades)
|
|
render_unauthorized_action and return
|
|
end
|
|
|
|
unless @assignment.moderated_grading?
|
|
return render :json => { :message => "Assignment does not use moderated grading" }, :status => :bad_request
|
|
end
|
|
if @assignment.grades_published?
|
|
return render :json => { :message => "Assignment grades have already been published" }, :status => :bad_request
|
|
end
|
|
|
|
submissions = @assignment.submissions.preload(:all_submission_comments,
|
|
{ :provisional_grades => :rubric_assessments })
|
|
selections = @assignment.moderated_grading_selections.index_by(&:student_id)
|
|
|
|
graded_submissions = submissions.select do |submission|
|
|
submission.provisional_grades.any?
|
|
end
|
|
|
|
grades_to_publish = graded_submissions.map do |submission|
|
|
if (selection = selections[submission.user_id])
|
|
# student in moderation: choose the selected provisional grade
|
|
selected_provisional_grade = submission.provisional_grades
|
|
.detect { |pg| pg.id == selection.selected_provisional_grade_id }
|
|
end
|
|
|
|
# either the student is not in moderation, or not all provisional grades were entered
|
|
# choose the first one with a grade (there should only be one)
|
|
unless selected_provisional_grade
|
|
provisional_grades = submission.provisional_grades
|
|
.select { |pg| pg.graded_at.present? }
|
|
selected_provisional_grade = provisional_grades.first if provisional_grades.count == 1
|
|
end
|
|
|
|
# We still don't have a provisional grade. Let's pick up the first blank provisional grade
|
|
# for this submission if it exists. This will happen as a result of commenting on a
|
|
# submission without grading it
|
|
selected_provisional_grade ||= submission.provisional_grades.detect { |pg| pg.graded_at.nil? }
|
|
|
|
unless selected_provisional_grade
|
|
return render json: { message: "All submissions must have a selected grade" },
|
|
status: :unprocessable_entity
|
|
end
|
|
|
|
selected_provisional_grade
|
|
end
|
|
|
|
grades_to_publish.each(&:publish!)
|
|
@context.touch_admins_later # just in case nothing got published
|
|
@assignment.update_attribute(:grades_published_at, Time.now.utc)
|
|
render :json => { :message => "OK" }
|
|
end
|
|
|
|
private
|
|
|
|
def load_assignment
|
|
@context = api_find(Course, params[:course_id])
|
|
@assignment = @context.assignments.active.find(params[:assignment_id])
|
|
end
|
|
end
|