fix speedgrader status menu with anonymous assignments
closes EVAL-1995 flag=edit_submission_status_from_speedgrader Test Plan: 1. Enable "Edit Submission Status from Speedgrader". 2. Create an anonymous assignment. 3. Go to SpeedGrader and change a student's status (i.e. Late, Missing, Excused, or None) using the select menu. Verify the request succeeds and the status is updated. 4. Create an assignment that is not anonymous. 5. Go to SpeedGrader and change a student's status using the select menu. Verify the request succeeds and the status is updated. Change-Id: I949648b7a2c78e01642d0c73298995acb3e4e7d6 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/275004 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Adrian Packel <apackel@instructure.com> Reviewed-by: Eduardo Escobar <eduardo.escobar@instructure.com> Reviewed-by: Syed Hussain <shussain@instructure.com> QA-Review: Kai Bjorkman <kbjorkman@instructure.com> Product-Review: Jody Sailor
This commit is contained in:
parent
b0af5c15fa
commit
734344eaff
|
@ -206,7 +206,7 @@
|
|||
#
|
||||
class SubmissionsApiController < ApplicationController
|
||||
before_action :get_course_from_section, :require_context, :require_user
|
||||
batch_jobs_in_actions :only => [:update], :batch => { :priority => Delayed::LOW_PRIORITY }
|
||||
batch_jobs_in_actions :only => [:update, :update_anonymous], :batch => { :priority => Delayed::LOW_PRIORITY }
|
||||
before_action :ensure_submission, :only => [:show,
|
||||
:document_annotations_read_state,
|
||||
:mark_document_annotations_read,
|
||||
|
@ -611,7 +611,15 @@ class SubmissionsApiController < ApplicationController
|
|||
@submission.assignment_visible_to_user?(@current_user)
|
||||
includes = Array(params[:include])
|
||||
@submission.visible_to_user = includes.include?("visibility") ? @assignment.visible_to_user?(@submission.user) : true
|
||||
render :json => submission_json(@submission, @assignment, @current_user, session, @context, includes, params)
|
||||
render json: submission_json(
|
||||
@submission,
|
||||
@assignment,
|
||||
@current_user,
|
||||
session,
|
||||
@context,
|
||||
includes,
|
||||
params.merge(anonymize_user_id: !!@anonymize_user_id)
|
||||
)
|
||||
else
|
||||
@unauthorized_message = t('#application.errors.submission_unauthorized', "You cannot access this submission.")
|
||||
return render_unauthorized_action
|
||||
|
@ -619,6 +627,20 @@ class SubmissionsApiController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# @API Get a single submission by anonymous id
|
||||
#
|
||||
# Get a single submission, based on the submission's anonymous id.
|
||||
#
|
||||
# @argument include[] [String, "submission_history"|"submission_comments"|"rubric_assessment"|"full_rubric_assessment"|"visibility"|"course"|"user"|"read_status"]
|
||||
# Associations to include with the group.
|
||||
def show_anonymous
|
||||
@assignment = api_find(@context.assignments.active, params[:assignment_id])
|
||||
@submission = @assignment.submissions.find_by!(anonymous_id: params[:anonymous_id])
|
||||
@user = get_user_considering_section(@submission.user_id)
|
||||
@anonymize_user_id = true
|
||||
show
|
||||
end
|
||||
|
||||
# @API Upload a file
|
||||
#
|
||||
# Upload a file to a submission.
|
||||
|
@ -785,7 +807,7 @@ class SubmissionsApiController < ApplicationController
|
|||
# Then a possible set of values for rubric_assessment would be:
|
||||
# rubric_assessment[crit1][points]=3&rubric_assessment[crit1][rating_id]=rat1&rubric_assessment[crit2][points]=5&rubric_assessment[crit2][rating_id]=rat2&rubric_assessment[crit2][comments]=Well%20Done.
|
||||
def update
|
||||
@assignment = api_find(@context.assignments.active, params[:assignment_id])
|
||||
@assignment ||= api_find(@context.assignments.active, params[:assignment_id])
|
||||
|
||||
if params[:submission] && params[:submission][:posted_grade] && !params[:submission][:provisional] &&
|
||||
@assignment.moderated_grading && !@assignment.grades_published?
|
||||
|
@ -793,9 +815,8 @@ class SubmissionsApiController < ApplicationController
|
|||
return
|
||||
end
|
||||
|
||||
@user = get_user_considering_section(params[:user_id])
|
||||
|
||||
@submission = @assignment.all_submissions.find_or_create_by!(user: @user)
|
||||
@user ||= get_user_considering_section(params[:user_id])
|
||||
@submission ||= @assignment.all_submissions.find_or_create_by!(user: @user)
|
||||
|
||||
authorized = if params[:submission] || params[:rubric_assessment]
|
||||
authorized_action(@submission, @current_user, :grade)
|
||||
|
@ -917,7 +938,15 @@ class SubmissionsApiController < ApplicationController
|
|||
user_ids = @submissions.map(&:user_id)
|
||||
users_with_visibility = AssignmentStudentVisibility.where(course_id: @context, assignment_id: @assignment, user_id: user_ids).pluck(:user_id).to_set
|
||||
end
|
||||
json = submission_json(@submission, @assignment, @current_user, session, @context, includes, params)
|
||||
json = submission_json(
|
||||
@submission,
|
||||
@assignment,
|
||||
@current_user,
|
||||
session,
|
||||
@context,
|
||||
includes,
|
||||
params.merge(anonymize_user_id: !!@anonymize_user_id)
|
||||
)
|
||||
|
||||
includes.delete("submission_comments")
|
||||
Version.preload_version_number(@submissions)
|
||||
|
@ -926,12 +955,141 @@ class SubmissionsApiController < ApplicationController
|
|||
submission.visible_to_user = users_with_visibility.include?(submission.user_id)
|
||||
end
|
||||
|
||||
submission_json(submission, @assignment, @current_user, session, @context, includes, params)
|
||||
submission_json(
|
||||
submission,
|
||||
@assignment,
|
||||
@current_user,
|
||||
session,
|
||||
@context,
|
||||
includes,
|
||||
params.merge(anonymize_user_id: !!@anonymize_user_id)
|
||||
)
|
||||
end
|
||||
render :json => json
|
||||
end
|
||||
end
|
||||
|
||||
# @API Grade or comment on a submission by anonymous id
|
||||
#
|
||||
# Comment on and/or update the grading for a student's assignment submission,
|
||||
# fetching the submission by anonymous id (instead of user id). If any
|
||||
# submission or rubric_assessment arguments are provided, the user must
|
||||
# have permission to manage grades in the appropriate context (course or
|
||||
# section).
|
||||
#
|
||||
# @argument comment[text_comment] [String]
|
||||
# Add a textual comment to the submission.
|
||||
#
|
||||
# @argument comment[group_comment] [Boolean]
|
||||
# Whether or not this comment should be sent to the entire group (defaults
|
||||
# to false). Ignored if this is not a group assignment or if no text_comment
|
||||
# is provided.
|
||||
#
|
||||
# @argument comment[media_comment_id] [String]
|
||||
# Add an audio/video comment to the submission. Media comments can be added
|
||||
# via this API, however, note that there is not yet an API to generate or
|
||||
# list existing media comments, so this functionality is currently of
|
||||
# limited use.
|
||||
#
|
||||
# @argument comment[media_comment_type] [String, "audio"|"video"]
|
||||
# The type of media comment being added.
|
||||
#
|
||||
# @argument comment[file_ids][] [Integer]
|
||||
# Attach files to this comment that were previously uploaded using the
|
||||
# Submission Comment API's files action
|
||||
#
|
||||
# @argument include[visibility] [String]
|
||||
# Whether this assignment is visible to the owner of the submission
|
||||
#
|
||||
# @argument submission[posted_grade] [String]
|
||||
# Assign a score to the submission, updating both the "score" and "grade"
|
||||
# fields on the submission record. This parameter can be passed in a few
|
||||
# different formats:
|
||||
#
|
||||
# points:: A floating point or integral value, such as "13.5". The grade
|
||||
# will be interpreted directly as the score of the assignment.
|
||||
# Values above assignment.points_possible are allowed, for awarding
|
||||
# extra credit.
|
||||
# percentage:: A floating point value appended with a percent sign, such as
|
||||
# "40%". The grade will be interpreted as a percentage score on the
|
||||
# assignment, where 100% == assignment.points_possible. Values above 100%
|
||||
# are allowed, for awarding extra credit.
|
||||
# letter grade:: A letter grade, following the assignment's defined letter
|
||||
# grading scheme. For example, "A-". The resulting score will be the high
|
||||
# end of the defined range for the letter grade. For instance, if "B" is
|
||||
# defined as 86% to 84%, a letter grade of "B" will be worth 86%. The
|
||||
# letter grade will be rejected if the assignment does not have a defined
|
||||
# letter grading scheme. For more fine-grained control of scores, pass in
|
||||
# points or percentage rather than the letter grade.
|
||||
# "pass/complete/fail/incomplete":: A string value of "pass" or "complete"
|
||||
# will give a score of 100%. "fail" or "incomplete" will give a score of
|
||||
# 0.
|
||||
#
|
||||
# Note that assignments with grading_type of "pass_fail" can only be
|
||||
# assigned a score of 0 or assignment.points_possible, nothing inbetween. If
|
||||
# a posted_grade in the "points" or "percentage" format is sent, the grade
|
||||
# will only be accepted if the grade equals one of those two values.
|
||||
#
|
||||
# @argument submission[excuse] [Boolean]
|
||||
# Sets the "excused" status of an assignment.
|
||||
#
|
||||
# @argument submission[late_policy_status] [String]
|
||||
# Sets the late policy status to either "late", "missing", "none", or null.
|
||||
#
|
||||
# @argument submission[seconds_late_override] [Integer]
|
||||
# Sets the seconds late if late policy status is "late"
|
||||
#
|
||||
# @argument rubric_assessment [RubricAssessment]
|
||||
# Assign a rubric assessment to this assignment submission. The
|
||||
# sub-parameters here depend on the rubric for the assignment. The general
|
||||
# format is, for each row in the rubric:
|
||||
#
|
||||
# The points awarded for this row.
|
||||
# rubric_assessment[criterion_id][points]
|
||||
#
|
||||
# The rating id for the row.
|
||||
# rubric_assessment[criterion_id][rating_id]
|
||||
#
|
||||
# Comments to add for this row.
|
||||
# rubric_assessment[criterion_id][comments]
|
||||
#
|
||||
#
|
||||
# For example, if the assignment rubric is (in JSON format):
|
||||
# !!!javascript
|
||||
# [
|
||||
# {
|
||||
# 'id': 'crit1',
|
||||
# 'points': 10,
|
||||
# 'description': 'Criterion 1',
|
||||
# 'ratings':
|
||||
# [
|
||||
# { 'id': 'rat1', 'description': 'Good', 'points': 10 },
|
||||
# { 'id': 'rat2', 'description': 'Poor', 'points': 3 }
|
||||
# ]
|
||||
# },
|
||||
# {
|
||||
# 'id': 'crit2',
|
||||
# 'points': 5,
|
||||
# 'description': 'Criterion 2',
|
||||
# 'ratings':
|
||||
# [
|
||||
# { 'id': 'rat1', 'description': 'Exemplary', 'points': 5 },
|
||||
# { 'id': 'rat2', 'description': 'Complete', 'points': 5 },
|
||||
# { 'id': 'rat3', 'description': 'Incomplete', 'points': 0 }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
#
|
||||
# Then a possible set of values for rubric_assessment would be:
|
||||
# rubric_assessment[crit1][points]=3&rubric_assessment[crit1][rating_id]=rat1&rubric_assessment[crit2][points]=5&rubric_assessment[crit2][rating_id]=rat2&rubric_assessment[crit2][comments]=Well%20Done.
|
||||
def update_anonymous
|
||||
@assignment = api_find(@context.assignments.active, params[:assignment_id])
|
||||
@submission = @assignment.submissions.find_by!(anonymous_id: params[:anonymous_id])
|
||||
@user = get_user_considering_section(@submission.user_id)
|
||||
@anonymize_user_id = true
|
||||
update
|
||||
end
|
||||
|
||||
# @API List gradeable students
|
||||
#
|
||||
# A paginated list of students eligible to submit the assignment. The caller must have permission to view grades.
|
||||
|
|
|
@ -1204,9 +1204,11 @@ CanvasRails::Application.routes.draw do
|
|||
get "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submissions", action: :index, as: "#{path_prefix}_assignment_submissions"
|
||||
get "#{context.pluralize}/:#{context}_id/students/submissions", controller: :submissions_api, action: :for_students, as: "#{path_prefix}_student_submissions"
|
||||
get "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submissions/:user_id", action: :show, as: "#{path_prefix}_assignment_submission"
|
||||
get "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/anonymous_submissions/:anonymous_id", action: :show_anonymous
|
||||
post "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submissions", action: :create, controller: :submissions
|
||||
post "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submissions/:user_id/files", action: :create_file
|
||||
put "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submissions/:user_id", action: :update
|
||||
put "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/anonymous_submissions/:anonymous_id", action: :update_anonymous
|
||||
post "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submissions/update_grades", action: :bulk_update
|
||||
get "#{context.pluralize}/:#{context}_id/assignments/:assignment_id/submission_summary", action: :submission_summary, as: "#{path_prefix}_assignment_submission_summary"
|
||||
end
|
||||
|
|
|
@ -132,7 +132,7 @@ module Api::V1::Submission
|
|||
end
|
||||
end
|
||||
|
||||
if context.account_membership_allows(current_user)
|
||||
if params[:anonymize_user_id] || context.account_membership_allows(current_user)
|
||||
hash['anonymous_id'] = submission.anonymous_id
|
||||
end
|
||||
|
||||
|
@ -164,6 +164,11 @@ module Api::V1::Submission
|
|||
other_fields -= params[:exclude_response_fields]
|
||||
end
|
||||
|
||||
if params[:anonymize_user_id]
|
||||
json_fields -= ["user_id"]
|
||||
json_fields << "anonymous_id"
|
||||
end
|
||||
|
||||
attempt.assignment = assignment
|
||||
hash = api_json(attempt, user, session, :only => json_fields, :methods => json_methods)
|
||||
if hash['body'].present?
|
||||
|
|
|
@ -2178,139 +2178,198 @@ describe 'Submissions API', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
context "show (differentiated_assignments)" do
|
||||
before do
|
||||
# set up course with DA and submit homework for an assignment
|
||||
# that is only visible to overrides for @section1
|
||||
# move student to a section that cannot see assignment by default
|
||||
describe "#show_anonymous" do
|
||||
before(:each) do
|
||||
@student = user_factory(active_all: true)
|
||||
course_with_teacher(:active_all => true)
|
||||
@section1 = @course.course_sections.create!(name: "test section")
|
||||
@section2 = @course.course_sections.create!(name: "test section")
|
||||
student_in_section(@section1, user: @student)
|
||||
@assignment = @course.assignments.create!(:title => 'assignment1', :grading_type => 'letter_grade', :points_possible => 15, :only_visible_to_overrides => true)
|
||||
create_section_override_for_assignment(@assignment, course_section: @section1)
|
||||
submit_homework(@assignment, @student)
|
||||
Score.where(enrollment_id: @student.enrollments).each(&:destroy_permanently!)
|
||||
@student.enrollments.each(&:destroy_permanently!)
|
||||
student_in_section(@section2, user: @student)
|
||||
|
||||
user_session(@student)
|
||||
section = @course.course_sections.create!(name: "test section")
|
||||
student_in_section(section, user: @student)
|
||||
@assignment = @course.assignments.create!(anonymous_grading: true)
|
||||
end
|
||||
|
||||
def call_to_submissions_show(opts = {})
|
||||
includes = %w(submission_comments rubric_assessment)
|
||||
includes.concat(opts[:includes]) if opts[:includes]
|
||||
helper_method = opts[:as_student] ? [:api_call_as_user, @student] : [:api_call]
|
||||
args = helper_method + [:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.to_param, :assignment_id => @assignment.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => includes }]
|
||||
self.send(*args)
|
||||
it "fetches the submission using the provided anonymous_id" do
|
||||
submission = @assignment.submissions.find_by(user: @student)
|
||||
|
||||
api_call(
|
||||
:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/anonymous_submissions/#{submission.anonymous_id}.json",
|
||||
{
|
||||
controller: 'submissions_api',
|
||||
action: 'show_anonymous',
|
||||
format: 'json',
|
||||
course_id: @course.to_param,
|
||||
assignment_id: @assignment.id.to_s,
|
||||
anonymous_id: submission.anonymous_id.to_s
|
||||
},
|
||||
{},
|
||||
{},
|
||||
{ expected_status: 200 }
|
||||
)
|
||||
end
|
||||
|
||||
context "as teacher" do
|
||||
context "with differentiated_assignments" do
|
||||
it "returns the assignment" do
|
||||
json = call_to_submissions_show(as_student: false)
|
||||
it "anonymizes the results" do
|
||||
submission = @assignment.submissions.find_by(user: @student)
|
||||
submission.submission_comments.create!(author: @student, comment: 'hi')
|
||||
|
||||
expect(json["assignment_id"]).not_to be_nil
|
||||
end
|
||||
json = api_call(
|
||||
:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/anonymous_submissions/#{submission.anonymous_id}.json",
|
||||
{
|
||||
controller: 'submissions_api',
|
||||
action: 'show_anonymous',
|
||||
format: 'json',
|
||||
course_id: @course.to_param,
|
||||
assignment_id: @assignment.id.to_s,
|
||||
anonymous_id: submission.anonymous_id.to_s
|
||||
},
|
||||
{ include: ['submission_comments', 'submission_history'], anonymize_user_id: true }
|
||||
)
|
||||
|
||||
it "returns assignment_visible" do
|
||||
json = call_to_submissions_show(as_student: false, includes: ["visibility"])
|
||||
expect(json["assignment_visible"]).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as student in a section without an override" do
|
||||
context "with differentiated_assignments" do
|
||||
it "returns an unauthorized error" do
|
||||
api_call_as_user(@student, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.to_param, :assignment_id => @assignment.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(submission_comments rubric_assessment) }, {}, expected_status: 401)
|
||||
end
|
||||
|
||||
it "returns the submission if it is graded" do
|
||||
@assignment.grade_student(@student, grade: 5, grader: @teacher)
|
||||
json = call_to_submissions_show(as_student: true)
|
||||
|
||||
expect(json["assignment_id"]).not_to be_nil
|
||||
end
|
||||
|
||||
it "returns assignment_visible false" do
|
||||
json = call_to_submissions_show(as_student: false, includes: ["visibility"])
|
||||
expect(json["assignment_visible"]).to eq(false)
|
||||
end
|
||||
aggregate_failures do
|
||||
expect(json).not_to have_key 'user_id'
|
||||
expect(json['anonymous_id']).to eq submission.anonymous_id
|
||||
expect(json.dig('submission_history', 0)).not_to have_key 'user_id'
|
||||
expect(json.dig('submission_history', 0, 'anonymous_id')).to eq submission.anonymous_id
|
||||
expect(json.dig('submission_comments', 0, 'author_id')).to be nil
|
||||
expect(json.dig('submission_comments', 0, 'author_name')).to eq 'Anonymous User'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "show full rubric assessments" do
|
||||
before do
|
||||
describe "#show" do
|
||||
before(:each) do
|
||||
@student = user_factory(active_all: true)
|
||||
course_with_teacher(active_all: true)
|
||||
course_with_teacher(:active_all => true)
|
||||
@section = @course.course_sections.create!(name: "test section")
|
||||
student_in_section(@section, user: @student)
|
||||
@assignment1 = assignment_model(context: @course)
|
||||
submit_homework(@assignment1, @student)
|
||||
end
|
||||
|
||||
it "fails when no rubric assessment is present" do
|
||||
json = api_call(:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment1.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment1.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) })
|
||||
expect(json).not_to have_key 'full_rubric_assessment'
|
||||
end
|
||||
|
||||
context "if present" do
|
||||
context "differentiated assignments" do
|
||||
before do
|
||||
@assignment2 = assignment_model(context: @course)
|
||||
rubric_assessment_model({ :purpose => "grading",
|
||||
:association_object => @assignment2,
|
||||
:user => @student,
|
||||
:assessment_type => "grading" })
|
||||
# set up course with DA and submit homework for an assignment
|
||||
# that is only visible to overrides for @section
|
||||
# move student to a section that cannot see assignment by default
|
||||
@section2 = @course.course_sections.create!(name: "test section")
|
||||
@assignment = @course.assignments.create!(:title => 'assignment1', :grading_type => 'letter_grade', :points_possible => 15, :only_visible_to_overrides => true)
|
||||
create_section_override_for_assignment(@assignment, course_section: @section)
|
||||
submit_homework(@assignment, @student)
|
||||
Score.where(enrollment_id: @student.enrollments).each(&:destroy_permanently!)
|
||||
@student.enrollments.each(&:destroy_permanently!)
|
||||
student_in_section(@section2, user: @student)
|
||||
|
||||
user_session(@student)
|
||||
end
|
||||
|
||||
it "returns the correct data" do
|
||||
json = api_call_as_user(@teacher, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment2.id}/submissions/#{@student.id}.json",
|
||||
def call_to_submissions_show(opts = {})
|
||||
includes = %w(submission_comments rubric_assessment)
|
||||
includes.concat(opts[:includes]) if opts[:includes]
|
||||
helper_method = opts[:as_student] ? [:api_call_as_user, @student] : [:api_call]
|
||||
args = helper_method + [:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment2.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) })
|
||||
expect(json).to have_key 'full_rubric_assessment'
|
||||
expect(json['full_rubric_assessment']).to have_key 'data'
|
||||
expect(json['full_rubric_assessment']).to have_key 'assessor_name'
|
||||
expect(json['full_rubric_assessment']).to have_key 'assessor_avatar_url'
|
||||
:format => 'json', :course_id => @course.to_param, :assignment_id => @assignment.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => includes }]
|
||||
self.send(*args)
|
||||
end
|
||||
|
||||
it "is visible to student owning the assignment" do
|
||||
json = api_call_as_user(@student, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment2.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment2.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) })
|
||||
expect(json['full_rubric_assessment']).not_to be_nil
|
||||
context "as teacher" do
|
||||
context "with differentiated_assignments" do
|
||||
it "returns the assignment" do
|
||||
json = call_to_submissions_show(as_student: false)
|
||||
|
||||
expect(json["assignment_id"]).not_to be_nil
|
||||
end
|
||||
|
||||
it "returns assignment_visible" do
|
||||
json = call_to_submissions_show(as_student: false, includes: ["visibility"])
|
||||
expect(json["assignment_visible"]).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is not visible to students that are not the owner of the assignment" do
|
||||
@other_student = user_factory(active_all: true)
|
||||
student_in_section(@section, user: @other_student)
|
||||
api_call_as_user(@other_student, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment2.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment2.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) }, {}, expected_status: 401)
|
||||
context "as student in a section without an override" do
|
||||
context "with differentiated_assignments" do
|
||||
it "returns an unauthorized error" do
|
||||
api_call_as_user(@student, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.to_param, :assignment_id => @assignment.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(submission_comments rubric_assessment) }, {}, expected_status: 401)
|
||||
end
|
||||
|
||||
it "returns the submission if it is graded" do
|
||||
@assignment.grade_student(@student, grade: 5, grader: @teacher)
|
||||
json = call_to_submissions_show(as_student: true)
|
||||
|
||||
expect(json["assignment_id"]).not_to be_nil
|
||||
end
|
||||
|
||||
it "returns assignment_visible false" do
|
||||
json = call_to_submissions_show(as_student: false, includes: ["visibility"])
|
||||
expect(json["assignment_visible"]).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "full rubric assessments" do
|
||||
before do
|
||||
@assignment1 = assignment_model(context: @course)
|
||||
submit_homework(@assignment1, @student)
|
||||
end
|
||||
|
||||
it "fails when no rubric assessment is present" do
|
||||
json = api_call(:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment1.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment1.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) })
|
||||
expect(json).not_to have_key 'full_rubric_assessment'
|
||||
end
|
||||
|
||||
context "if present" do
|
||||
before do
|
||||
@assignment2 = assignment_model(context: @course)
|
||||
rubric_assessment_model({ :purpose => "grading",
|
||||
:association_object => @assignment2,
|
||||
:user => @student,
|
||||
:assessment_type => "grading" })
|
||||
end
|
||||
|
||||
it "returns the correct data" do
|
||||
json = api_call_as_user(@teacher, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment2.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment2.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) })
|
||||
expect(json).to have_key 'full_rubric_assessment'
|
||||
expect(json['full_rubric_assessment']).to have_key 'data'
|
||||
expect(json['full_rubric_assessment']).to have_key 'assessor_name'
|
||||
expect(json['full_rubric_assessment']).to have_key 'assessor_avatar_url'
|
||||
end
|
||||
|
||||
it "is visible to student owning the assignment" do
|
||||
json = api_call_as_user(@student, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment2.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment2.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) })
|
||||
expect(json['full_rubric_assessment']).not_to be_nil
|
||||
end
|
||||
|
||||
it "is not visible to students that are not the owner of the assignment" do
|
||||
@other_student = user_factory(active_all: true)
|
||||
student_in_section(@section, user: @other_student)
|
||||
api_call_as_user(@other_student, :get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment2.id}/submissions/#{@student.id}.json",
|
||||
{ :controller => 'submissions_api', :action => 'show',
|
||||
:format => 'json', :course_id => @course.id.to_s,
|
||||
:assignment_id => @assignment2.id.to_s, :user_id => @student.id.to_s },
|
||||
{ :include => %w(full_rubric_assessment) }, {}, expected_status: 401)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2888,6 +2947,71 @@ describe 'Submissions API', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#update_anonymous" do
|
||||
before :once do
|
||||
student_in_course(active_all: true)
|
||||
teacher_in_course(active_all: true)
|
||||
@assignment = @course.assignments.create!(
|
||||
title: 'assignment1',
|
||||
anonymous_grading: true,
|
||||
grading_type: 'letter_grade',
|
||||
points_possible: 15
|
||||
)
|
||||
end
|
||||
|
||||
it "fetches the submission using the provided anonymous_id" do
|
||||
submission = @assignment.submission_for_student(@student)
|
||||
|
||||
expect {
|
||||
api_call(
|
||||
:put,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/anonymous_submissions/#{submission.anonymous_id}.json",
|
||||
{
|
||||
controller: 'submissions_api',
|
||||
action: 'update_anonymous',
|
||||
format: 'json',
|
||||
course_id: @course.id.to_s,
|
||||
assignment_id: @assignment.id.to_s,
|
||||
anonymous_id: submission.anonymous_id.to_s
|
||||
}, {
|
||||
submission: { posted_grade: 'B' }
|
||||
}
|
||||
)
|
||||
}.to change {
|
||||
submission.reload.grade
|
||||
}.from(nil).to('B')
|
||||
end
|
||||
|
||||
it "anonymizes the results" do
|
||||
submission = @assignment.submission_for_student(@student)
|
||||
submission.submission_comments.create!(author: @student, comment: 'hi')
|
||||
|
||||
json = api_call(
|
||||
:put,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/anonymous_submissions/#{submission.anonymous_id}.json",
|
||||
{
|
||||
controller: 'submissions_api',
|
||||
action: 'update_anonymous',
|
||||
format: 'json',
|
||||
course_id: @course.id.to_s,
|
||||
assignment_id: @assignment.id.to_s,
|
||||
anonymous_id: submission.anonymous_id.to_s
|
||||
}, {
|
||||
submission: { posted_grade: 'B' }
|
||||
}
|
||||
)
|
||||
|
||||
aggregate_failures do
|
||||
expect(json).not_to have_key 'user_id'
|
||||
expect(json['anonymous_id']).to eq submission.anonymous_id
|
||||
expect(json.dig('all_submissions', 0)).not_to have_key 'user_id'
|
||||
expect(json.dig('all_submissions', 0, 'anonymous_id')).to eq submission.anonymous_id
|
||||
expect(json.dig('submission_comments', 0, 'author_id')).to be nil
|
||||
expect(json.dig('submission_comments', 0, 'author_name')).to eq 'Anonymous User'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update' do
|
||||
before :once do
|
||||
student_in_course(:active_all => true)
|
||||
|
|
|
@ -550,12 +550,16 @@ QUnit.module('SpeedGrader', rootHooks => {
|
|||
{
|
||||
grade: 70,
|
||||
score: 7,
|
||||
user_id: '4'
|
||||
user_id: '4',
|
||||
assignment_id: '1',
|
||||
anonymous_id: 'i9Z1a'
|
||||
},
|
||||
{
|
||||
grade: 10,
|
||||
score: 1,
|
||||
user_id: '5'
|
||||
user_id: '5',
|
||||
assignment_id: '1',
|
||||
anonymous_id: 't4N2y'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -579,7 +583,8 @@ QUnit.module('SpeedGrader', rootHooks => {
|
|||
|
||||
test('makes request to API', () => {
|
||||
SpeedGrader.EG.refreshGrades()
|
||||
ok($.getJSON.calledWithMatch('submission_history'))
|
||||
const request = $.getJSON.lastCall
|
||||
strictEqual(request.args[1]['include[]'], 'submission_history')
|
||||
})
|
||||
|
||||
test('updates the submission for the requested student', () => {
|
||||
|
@ -600,7 +605,7 @@ QUnit.module('SpeedGrader', rootHooks => {
|
|||
|
||||
test('does not call showGrade if a different student has been selected since the request', () => {
|
||||
$.getJSON.restore()
|
||||
sinon.stub($, 'getJSON').callsFake((url, successCallback) => {
|
||||
sinon.stub($, 'getJSON').callsFake((url, params, successCallback) => {
|
||||
SpeedGrader.EG.currentStudent = window.jsonData.studentMap['5']
|
||||
successCallback({user_id: '4', score: 2, grade: '20'})
|
||||
})
|
||||
|
@ -1768,7 +1773,7 @@ QUnit.module('SpeedGrader', rootHooks => {
|
|||
}
|
||||
selectStatusMenuOption(optionsIndexes.Late)
|
||||
|
||||
const getJsonStub = sinon.stub($, 'getJSON').callsFake((_url, successCallback) => {
|
||||
const getJsonStub = sinon.stub($, 'getJSON').callsFake((_url, _data, successCallback) => {
|
||||
successCallback(responseRefreshRequest)
|
||||
|
||||
moxios.uninstall()
|
||||
|
@ -1839,7 +1844,7 @@ QUnit.module('SpeedGrader', rootHooks => {
|
|||
}
|
||||
selectStatusMenuOption(optionsIndexes.Missing)
|
||||
|
||||
const getJsonStub = sinon.stub($, 'getJSON').callsFake((_url, successCallback) => {
|
||||
const getJsonStub = sinon.stub($, 'getJSON').callsFake((_url, _data, successCallback) => {
|
||||
successCallback(responseRefreshRequest)
|
||||
|
||||
moxios.uninstall()
|
||||
|
@ -1910,7 +1915,7 @@ QUnit.module('SpeedGrader', rootHooks => {
|
|||
}
|
||||
selectStatusMenuOption(optionsIndexes.Excused)
|
||||
|
||||
const getJsonStub = sinon.stub($, 'getJSON').callsFake((_url, successCallback) => {
|
||||
const getJsonStub = sinon.stub($, 'getJSON').callsFake((_url, _data, successCallback) => {
|
||||
successCallback(responseRefreshRequest)
|
||||
|
||||
moxios.uninstall()
|
||||
|
|
|
@ -92,9 +92,19 @@ describe Api::V1::Submission do
|
|||
end
|
||||
|
||||
context 'when not an account user' do
|
||||
it 'does not include anonymous_id' do
|
||||
it 'does not include anonymous_id by default' do
|
||||
expect(json).not_to have_key 'anonymous_id'
|
||||
end
|
||||
|
||||
it 'includes anonymous_id when passed anonymize_user_id: true' do
|
||||
params[:anonymize_user_id] = true
|
||||
expect(json['anonymous_id']).to eq submission.anonymous_id
|
||||
end
|
||||
|
||||
it 'excludes user_id when passed anonymize_user_id: true' do
|
||||
params[:anonymize_user_id] = true
|
||||
expect(json).not_to have_key 'user_id'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an account user' do
|
||||
|
|
|
@ -40,12 +40,21 @@ export function determineSubmissionSelection(submission) {
|
|||
}
|
||||
}
|
||||
|
||||
export function makeSubmissionUpdateRequest(submission, courseId, data) {
|
||||
const requestData = {
|
||||
export function makeSubmissionUpdateRequest(submission, isAnonymous, courseId, updateData) {
|
||||
const data = {}
|
||||
const submissionData = {
|
||||
assignmentId: submission.assignment_id,
|
||||
userId: submission.user_id,
|
||||
...data
|
||||
...updateData
|
||||
}
|
||||
const url = `/api/v1/courses/${courseId}/assignments/${submission.assignment_id}/submissions/${submission.user_id}`
|
||||
return axios.put(url, {submission: underscore(requestData)})
|
||||
|
||||
let url
|
||||
if (isAnonymous) {
|
||||
url = `/api/v1/courses/${courseId}/assignments/${submission.assignment_id}/anonymous_submissions/${submission.anonymous_id}`
|
||||
} else {
|
||||
submissionData.userId = submission.user_id
|
||||
url = `/api/v1/courses/${courseId}/assignments/${submission.assignment_id}/submissions/${submission.user_id}`
|
||||
}
|
||||
|
||||
data.submission = underscore(submissionData)
|
||||
return axios.put(url, data)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ describe('determineSubmissionSelection', () => {
|
|||
|
||||
describe('makeSubmissionUpdateRequest', () => {
|
||||
let data
|
||||
let isAnonymous
|
||||
let courseId
|
||||
let submission
|
||||
|
||||
|
@ -69,6 +70,7 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
data = {latePolicyStatus: 'none'}
|
||||
isAnonymous = false
|
||||
courseId = 1
|
||||
submission = {
|
||||
assignment_id: 2,
|
||||
|
@ -84,7 +86,7 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
})
|
||||
|
||||
it('makes a request to the proper endpoint', function (done) {
|
||||
makeSubmissionUpdateRequest(submission, courseId, data)
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, courseId, data)
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(request.url).toEqual('/api/v1/courses/1/assignments/2/submissions/3')
|
||||
|
@ -92,6 +94,19 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('makes a request to the "anonymous" endpoint if the assignment is anonymous', function (done) {
|
||||
isAnonymous = true
|
||||
submission.anonymous_id = 'i9Z1a'
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, courseId, data)
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(request.url).toEqual(
|
||||
`/api/v1/courses/1/assignments/2/anonymous_submissions/${submission.anonymous_id}`
|
||||
)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('makes a request with the expected params underscored properly when submission status is "none"', function (done) {
|
||||
const expectedData = {
|
||||
submission: {
|
||||
|
@ -101,7 +116,7 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
}
|
||||
}
|
||||
|
||||
makeSubmissionUpdateRequest(submission, courseId, data)
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, courseId, data)
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(JSON.parse(request.config.data)).toEqual(expectedData)
|
||||
|
@ -120,7 +135,7 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
}
|
||||
}
|
||||
|
||||
makeSubmissionUpdateRequest(submission, courseId, data)
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, courseId, data)
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(JSON.parse(request.config.data)).toEqual(expectedData)
|
||||
|
@ -140,7 +155,7 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
}
|
||||
}
|
||||
|
||||
makeSubmissionUpdateRequest(submission, courseId, data)
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, courseId, data)
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(JSON.parse(request.config.data)).toEqual(expectedData)
|
||||
|
@ -159,7 +174,7 @@ describe('makeSubmissionUpdateRequest', () => {
|
|||
}
|
||||
}
|
||||
|
||||
makeSubmissionUpdateRequest(submission, courseId, data)
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, courseId, data)
|
||||
moxios.wait(() => {
|
||||
const request = moxios.requests.mostRecent()
|
||||
expect(JSON.parse(request.config.data)).toEqual(expectedData)
|
||||
|
|
|
@ -1032,9 +1032,11 @@ function refreshGrades(callback) {
|
|||
const courseId = ENV.course_id
|
||||
const assignmentId = EG.currentStudent.submission.assignment_id
|
||||
const studentId = EG.currentStudent.submission[anonymizableUserId]
|
||||
const url = `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${studentId}.json?include[]=submission_history`
|
||||
const resourceSegment = isAnonymous ? 'anonymous_submissions' : 'submissions'
|
||||
const params = {'include[]': 'submission_history'}
|
||||
const url = `/api/v1/courses/${courseId}/assignments/${assignmentId}/${resourceSegment}/${studentId}.json`
|
||||
const currentStudentIDAsOfAjaxCall = EG.currentStudent[anonymizableId]
|
||||
$.getJSON(url, submission => {
|
||||
$.getJSON(url, params, submission => {
|
||||
const studentToRefresh = window.jsonData.studentMap[currentStudentIDAsOfAjaxCall]
|
||||
EG.setOrUpdateSubmission(submission)
|
||||
|
||||
|
@ -1184,7 +1186,7 @@ function getLateMissingAndExcusedPills() {
|
|||
function updateSubmissionAndPageEffects(data) {
|
||||
const submission = EG.currentStudent.submission
|
||||
|
||||
makeSubmissionUpdateRequest(submission, ENV.course_id, data)
|
||||
makeSubmissionUpdateRequest(submission, isAnonymous, ENV.course_id, data)
|
||||
.then(() => {
|
||||
refreshGrades(() => {
|
||||
EG.showSubmissionDetails()
|
||||
|
|
Loading…
Reference in New Issue