Skip to content

Commit

Permalink
Enable grading to go through submissions in randomized order (#6641)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielDervishi authored Jul 6, 2023
1 parent 94b43e9 commit 494bfa3
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 1 deletion.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions app/assets/javascripts/Components/Result/result.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}
/>
Expand Down
20 changes: 20 additions & 0 deletions app/assets/javascripts/Components/Result/submission_selector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ export class SubmissionSelector extends React.Component {
);
}

renderRandomIncompleteSubmissionButton() {
if (this.props.role !== "Student") {
return (
<button
className="button random-incomplete-submission"
onClick={this.props.randomIncompleteSubmission}
title={`${I18n.t("results.random_incomplete_submission")} (Ctrl + Shift + ⇨)`}
disabled={
this.props.num_collected === this.props.num_marked ||
(this.props.marking_state === "incomplete" &&
this.props.num_marked === this.props.num_collected - 1)
}
>
<FontAwesomeIcon icon="fa-solid fa-dice" className="no-padding" />
</button>
);
}
}

render() {
if (this.props.role === "Student" && !this.props.is_reviewer) {
return "";
Expand Down Expand Up @@ -138,6 +157,7 @@ export class SubmissionSelector extends React.Component {
>
<FontAwesomeIcon icon="fa-solid fa-arrow-right" className="no-padding" />
</button>
{this.renderRandomIncompleteSubmissionButton()}
<div className="progress">
<meter
value={this.props.num_marked}
Expand Down
8 changes: 8 additions & 0 deletions app/assets/javascripts/Results/keybinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ Mousetrap.bind("shift+right", function () {
}
});

// Go to a random incomplete submission with ctrl + shift + right
Mousetrap.bind("ctrl+shift+right", function () {
// Don't override range selection keybindings
if (!is_text_selected()) {
$(".button.random-incomplete-submission")[0].click();
}
});

// Go to the previous criterion with shift + up
Mousetrap.bind("shift+up", function (e) {
if (!is_text_selected()) {
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/results_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,17 @@ def next_grouping
render json: { next_result: next_result, next_grouping: next_grouping }
end

def random_incomplete_submission
result = record
grouping = result.submission.grouping

next_grouping = grouping.get_random_incomplete(current_role)
next_result = next_grouping&.current_result

render json: { result_id: next_result&.id, submission_id: next_result&.submission_id,
grouping_id: next_grouping&.id }
end

def set_released_to_students
@result = record
released_to_students = !@result.released_to_students
Expand Down
17 changes: 17 additions & 0 deletions app/models/grouping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,23 @@ def get_next_grouping(current_role, reversed)
next_grouping
end

def get_random_incomplete(current_role)
if current_role.ta?
# Get relevant groupings for a TA
results = self.assignment.current_results.joins(grouping: :tas).where(
marking_state: Result::MARKING_STATES[:incomplete],
'roles.id': current_role.id
)

else
# Get all groupings in an assignment -- typically for an instructor
results = self.assignment.current_results.where(
marking_state: Result::MARKING_STATES[:incomplete]
)
end
results.where.not('groupings.id': self.id).order('RANDOM()').first&.grouping
end

private

def add_assignment_folder(group_repo)
Expand Down
2 changes: 1 addition & 1 deletion app/policies/result_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class ResultPolicy < ApplicationPolicy
default_rule :manage?
alias_rule :get_test_runs_instructors_released?, to: :view_marks?
alias_rule :create?, :add_extra_mark?, :remove_extra_mark?, :get_test_runs_instructors?,
:add_tag?, :remove_tag?, :revert_to_automatic_deductions?, to: :grade?
:add_tag?, :remove_tag?, :revert_to_automatic_deductions?, :random_incomplete_submission?, to: :grade?
alias_rule :show?, :get_annotations?, :print?, to: :view?
alias_rule :edit?, :update_mark?, :toggle_marking_state?, :update_overall_comment?, :next_grouping?, to: :review?
alias_rule :refresh_view_tokens?, :update_view_token_expiry?, to: :set_released_to_students?
Expand Down
6 changes: 6 additions & 0 deletions app/views/results/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
<td><kbd>Shift</kbd> + <kbd></kbd></td>
<td><%=t('results.previous_submission')%></td>
</tr>
<%if @current_role.class.name != 'Student'%>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd></kbd></td>
<td><%=t('results.random_incomplete_submission')%></td>
</tr>
<%end%>
<tr>
<td><kbd>Shift</kbd> + <kbd></kbd></td>
<td><%=t('results.keybinding.previous_criterion')%></td>
Expand Down
2 changes: 2 additions & 0 deletions config/locales/views/results/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
43 changes: 43 additions & 0 deletions spec/controllers/results_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
Expand Down
49 changes: 49 additions & 0 deletions spec/models/grouping_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 494bfa3

Please sign in to comment.