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:
Spencer Olson 2021-10-01 14:11:53 -05:00
parent b0af5c15fa
commit 734344eaff
9 changed files with 469 additions and 139 deletions

View File

@ -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.

View File

@ -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

View File

@ -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?

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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()