diff --git a/app/controllers/submissions_api_controller.rb b/app/controllers/submissions_api_controller.rb index 769db20b6b0..e2ce833c185 100644 --- a/app/controllers/submissions_api_controller.rb +++ b/app/controllers/submissions_api_controller.rb @@ -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. diff --git a/config/routes.rb b/config/routes.rb index 8346bca9107..492f15bd9b7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lib/api/v1/submission.rb b/lib/api/v1/submission.rb index c6085d838d1..01c7e3f11e1 100644 --- a/lib/api/v1/submission.rb +++ b/lib/api/v1/submission.rb @@ -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? diff --git a/spec/apis/v1/submissions_api_spec.rb b/spec/apis/v1/submissions_api_spec.rb index 02c754e659b..a41ccb2a77e 100644 --- a/spec/apis/v1/submissions_api_spec.rb +++ b/spec/apis/v1/submissions_api_spec.rb @@ -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) diff --git a/spec/javascripts/jsx/speed_graderSpec.js b/spec/javascripts/jsx/speed_graderSpec.js index 0e7717a88b4..5c692147c1e 100644 --- a/spec/javascripts/jsx/speed_graderSpec.js +++ b/spec/javascripts/jsx/speed_graderSpec.js @@ -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() diff --git a/spec/lib/api/v1/submission_spec.rb b/spec/lib/api/v1/submission_spec.rb index 993d44fa296..64fb7142ba4 100644 --- a/spec/lib/api/v1/submission_spec.rb +++ b/spec/lib/api/v1/submission_spec.rb @@ -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 diff --git a/ui/features/speed_grader/SpeedGraderStatusMenuHelpers.js b/ui/features/speed_grader/SpeedGraderStatusMenuHelpers.js index 747c41949ef..cda0e605a8a 100644 --- a/ui/features/speed_grader/SpeedGraderStatusMenuHelpers.js +++ b/ui/features/speed_grader/SpeedGraderStatusMenuHelpers.js @@ -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) } diff --git a/ui/features/speed_grader/__tests__/SpeedGraderStatusMenuHelpers.test.js b/ui/features/speed_grader/__tests__/SpeedGraderStatusMenuHelpers.test.js index 4aade6f24c1..3a94ec2d294 100644 --- a/ui/features/speed_grader/__tests__/SpeedGraderStatusMenuHelpers.test.js +++ b/ui/features/speed_grader/__tests__/SpeedGraderStatusMenuHelpers.test.js @@ -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) diff --git a/ui/features/speed_grader/jquery/speed_grader.js b/ui/features/speed_grader/jquery/speed_grader.js index 8befd4da0e6..fdc6587a893 100644 --- a/ui/features/speed_grader/jquery/speed_grader.js +++ b/ui/features/speed_grader/jquery/speed_grader.js @@ -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()