diff --git a/Changelog.md b/Changelog.md index 53af2036dd..ce18755177 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,6 +24,7 @@ - Add zoom feature to scanned exam template crop selection (#6640) - Ensure Jupyter notebook HTML rendering does not require external CDNs (#6656) - Prevent Jupyter notebook from reloading when an annotation is added (#6656) +- Added a button allowing graders to view a random incomplete submission (#6641) ## [v2.2.3] - Fix bug where in some circumstances the wrong result would be displayed to students (#6465) diff --git a/app/assets/javascripts/Components/Result/result.jsx b/app/assets/javascripts/Components/Result/result.jsx index fa335eec41..6ad5f9e2c7 100644 --- a/app/assets/javascripts/Components/Result/result.jsx +++ b/app/assets/javascripts/Components/Result/result.jsx @@ -677,6 +677,38 @@ class Result extends React.Component { }; }; + randomIncompleteSubmission = () => { + const url = Routes.random_incomplete_submission_course_result_path( + this.props.course_id, + this.state.result_id + ); + + this.setState({loading: true}, () => { + fetch(url) + .then(response => { + if (response.ok) { + return response.json(); + } + }) + .then(result => { + if (!result.result_id || !result.submission_id || !result.grouping_id) { + alert(I18n.t("results.no_incomplete_submission")); + this.setState({loading: false}); + return; + } + + const result_obj = { + result_id: result.result_id, + submission_id: result.submission_id, + grouping_id: result.grouping_id, + }; + this.setState(prevState => ({...prevState, ...result_obj})); + let new_url = Routes.edit_course_result_path(this.props.course_id, this.state.result_id); + history.pushState({}, document.title, new_url); + }); + }); + }; + updateOverallComment = (value, remark) => { return $.post({ url: Routes.update_overall_comment_course_result_path( @@ -731,6 +763,7 @@ class Result extends React.Component { toggleMarkingState={this.toggleMarkingState} setReleasedToStudents={this.setReleasedToStudents} nextSubmission={this.nextSubmission(1)} + randomIncompleteSubmission={this.randomIncompleteSubmission} previousSubmission={this.nextSubmission(-1)} course_id={this.props.course_id} /> diff --git a/app/assets/javascripts/Components/Result/submission_selector.jsx b/app/assets/javascripts/Components/Result/submission_selector.jsx index 10be93e964..a1c5c3ff4f 100644 --- a/app/assets/javascripts/Components/Result/submission_selector.jsx +++ b/app/assets/javascripts/Components/Result/submission_selector.jsx @@ -95,6 +95,25 @@ export class SubmissionSelector extends React.Component { ); } + renderRandomIncompleteSubmissionButton() { + if (this.props.role !== "Student") { + return ( + + ); + } + } + render() { if (this.props.role === "Student" && !this.props.is_reviewer) { return ""; @@ -138,6 +157,7 @@ export class SubmissionSelector extends React.Component { > + {this.renderRandomIncompleteSubmissionButton()}
Shift + <%=t('results.previous_submission')%> + <%if @current_role.class.name != 'Student'%> + + Ctrl + Shift + + <%=t('results.random_incomplete_submission')%> + + <%end%> Shift + <%=t('results.keybinding.previous_criterion')%> diff --git a/config/locales/views/results/en.yml b/config/locales/views/results/en.yml index a9638cd728..9a02e189ca 100644 --- a/config/locales/views/results/en.yml +++ b/config/locales/views/results/en.yml @@ -47,11 +47,13 @@ en: marks_released: The marks have been released. You cannot change the grades. next_submission: Next Submission no_feedback_files: "[No feedback files available]" + no_incomplete_submission: There are no more incomplete submissions! no_result: No marks are available yet. no_results_in_direction: There are no more results in this direction! overridden_deductions: Overridden previous_submission: Previous Submission print: Print + random_incomplete_submission: Random Incomplete Submission remark: about_remark_save: Please note that the instructor will be unaware of your saved request unless it has been submitted. about_remark_submission: Once a remark request has been submitted, you will not be able to see your original results until remark results are available. You will also not be able to edit the request. diff --git a/config/routes.rb b/config/routes.rb index 2852deedb4..b1e00e9eaa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -184,6 +184,7 @@ patch 'update_mark' get 'view_marks' post 'add_tag' + get 'random_incomplete_submission' post 'remove_tag' post 'run_tests' get 'stop_test' diff --git a/spec/controllers/results_controller_spec.rb b/spec/controllers/results_controller_spec.rb index ff0817479c..6e53d126f0 100644 --- a/spec/controllers/results_controller_spec.rb +++ b/spec/controllers/results_controller_spec.rb @@ -416,6 +416,7 @@ def self.test_unauthorized(route_name) add_extra_mark: :post, delete_grace_period_deduction: :delete, next_grouping: :get, + random_incomplete_submission: :get, remove_extra_mark: :post, revert_to_automatic_deductions: :patch, set_released_to_students: :post, @@ -437,6 +438,7 @@ def self.test_unauthorized(route_name) before(:each) { sign_in student } [:edit, :next_grouping, + :random_incomplete_submission, :set_released_to_students, :toggle_marking_state, :update_overall_comment, @@ -931,6 +933,39 @@ def self.test_unauthorized(route_name) expect(filename).to eq "#{assignment.short_identifier}_#{complete_result.grouping.group.group_name}.pdf" end end + + describe '#random_incomplete_submission' do + it 'should receive 200 when current grouping has a submission' do + allow_any_instance_of(Grouping).to receive(:has_submission).and_return true + get :random_incomplete_submission, params: { course_id: course.id, grouping_id: grouping.id, + id: incomplete_result.id } + expect(response).to have_http_status(:ok) + end + + it 'should receive 200 when current grouping does not have a submission' do + allow_any_instance_of(Grouping).to receive(:has_submission).and_return false + get :random_incomplete_submission, params: { course_id: course.id, grouping_id: grouping.id, + id: incomplete_result.id } + expect(response).to have_http_status(:ok) + end + context 'when there are no more random incomplete submissions' do + it 'should receive a JSON object with result_id, submission_id and grouping_id as nil' do + a2 = create(:assignment_with_criteria_and_results) + a2.groupings.each do |group| + group.tas.push(ta) + group.save + end + a2.save + get :random_incomplete_submission, params: { course_id: course.id, + grouping_id: a2.groupings.first.id, + id: a2.submissions.first.current_result.id } + expect(response).to have_http_status(:ok) + expect(response.parsed_body['result_id']).to eq(nil) + expect(response.parsed_body['submission_id']).to eq(nil) + expect(response.parsed_body['grouping_id']).to eq(nil) + end + end + end end context 'A TA' do before(:each) { sign_in ta } @@ -1089,6 +1124,14 @@ def self.test_unauthorized(route_name) expect(response).to have_http_status(:forbidden) } end + context 'accessing random_incomplete_submission' do + it { + allow_any_instance_of(Grouping).to receive(:has_submission).and_return true + get :random_incomplete_submission, + params: { course_id: course.id, grouping_id: grouping.id, id: incomplete_result.id } + expect(response).to have_http_status(:forbidden) + } + end context 'accessing add_tag' do before(:each) do tag = create(:tag) diff --git a/spec/models/grouping_spec.rb b/spec/models/grouping_spec.rb index f1e338252c..9b1402bb67 100644 --- a/spec/models/grouping_spec.rb +++ b/spec/models/grouping_spec.rb @@ -1611,4 +1611,53 @@ def expect_updated_criteria_coverage_count_eq(expected_count) end end end + describe '#get_random_incomplete' do + let!(:grouping1) { create :grouping_with_inviter_and_submission } + let!(:grouping2) { create :grouping_with_inviter_and_submission, assignment: grouping1.assignment } + let!(:grouping3) { create :grouping_with_inviter_and_submission, assignment: grouping1.assignment } + let!(:grouping4) { create :grouping, assignment: grouping1.assignment } + + shared_examples 'role is an instructor or ta' do + context 'there are no more incomplete submissions accessible to role' do + it 'returns nil' do + grouping1.current_result.update(marking_state: Result::MARKING_STATES[:complete]) + grouping2.current_result.update(marking_state: Result::MARKING_STATES[:complete]) + expect(grouping3.get_random_incomplete(role)).to be_nil + end + end + it 'shouldn\'t consider returning this grouping' do + expect(grouping2.get_random_incomplete(role).id).not_to eq(grouping2.id) + end + + it 'shouldn\'t consider returning groupings without submissions' do + expect(grouping2.get_random_incomplete(role).id).not_to eq(grouping4.id) + end + end + context 'when role is a ta' do + let!(:role) { create :ta } + before(:each) do + create :ta_membership, grouping: grouping2, role: role + create :ta_membership, grouping: grouping3, role: role + end + + it 'should only pick a random group from the groupings role can grade' do + expect(grouping2.get_random_incomplete(role).id).to eq(grouping3.id) + end + + it 'should only pick a random group from the groupings whose current result is incomplete' do + create :ta_membership, grouping: grouping1, role: role + grouping3.current_result.update(marking_state: Result::MARKING_STATES[:complete]) + expect(grouping2.get_random_incomplete(role).id).to eq(grouping1.id) + end + include_examples 'role is an instructor or ta' + end + context 'when role is an instructor' do + let!(:role) { create :instructor } + it 'should pick from all groupings with an incomplete marking_state (that isn\'t the current grouping)' do + grouping1.current_result.update(marking_state: Result::MARKING_STATES[:complete]) + expect(grouping2.get_random_incomplete(role).id).to eq(grouping3.id) + end + include_examples 'role is an instructor or ta' + end + end end