Skip to content

Commit

Permalink
Enable assigning graders by section (#7179)
Browse files Browse the repository at this point in the history
  • Loading branch information
AinaMerch committed Sep 18, 2024
1 parent 89fc20e commit 0a03ff5
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 7 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- Improve textviewer rendering speed (#7211)
- Add periodic roster syncing via LTI (#7178)
- Allow instructors to assign graders by section (#7179)

### 🐛 Bug fixes
- Fix incorrect calculation of token penalties when submissions are on time (#7216)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from "react";
import Modal from "react-modal";
import PropTypes from "prop-types";

export class SectionDistributionModal extends React.Component {
static defaultProps = {
override: false,
};

constructor(props) {
super(props);
this.input = React.createRef();
this.sectionsArray = Object.values(this.props.sections).sort();
this.graderMap = this.props.graders.reduce((map, grader) => {
map[grader.user_name] = grader._id;
return map;
}, {});
}

componentDidMount() {
Modal.setAppElement("body");
}

onSubmit = event => {
event.preventDefault();
const form = new FormData(this.input.current);
const assignments = {};
form.forEach((value, key) => {
assignments[key] = this.graderMap[value];
});
this.props.onSubmit(assignments);
};

renderSectionRow = section => {
const {graders} = this.props;
return (
<div className="flex-row-expand" key={section}>
<label htmlFor={`input-${section}`} className="modal-inline-label">
{section}
</label>
<select className={`input-${section}`} name={section}>
<option value=""></option>
{graders.map(grader => (
<option key={grader.user_name} value={grader.user_name}>
{grader.user_name}
</option>
))}
</select>
</div>
);
};

render() {
return (
<Modal
className="react-modal dialog"
isOpen={this.props.isOpen}
onRequestClose={this.props.onRequestClose}
>
<form onSubmit={this.onSubmit} ref={this.input}>
<div className={"modal-container-vertical"}>
<h2>{I18n.t("graders.assign_by_section_modal_title")}</h2>
<p style={{"max-width": "300px"}}>{I18n.t("graders.assign_by_section_instruction")}</p>
{this.sectionsArray.map(section => this.renderSectionRow(section))}
</div>
<div className={"modal-container"}>
<input type="submit" value={I18n.t("graders.actions.assign_by_section")} />
</div>
</form>
</Modal>
);
}
}

SectionDistributionModal.propTypes = {
graders: PropTypes.arrayOf(PropTypes.object).isRequired,
isOpen: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
sections: PropTypes.objectOf(PropTypes.string).isRequired,
};
41 changes: 41 additions & 0 deletions app/assets/javascripts/Components/graders_manager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {withSelection, CheckboxTable} from "./markus_with_selection_hoc";
import {selectFilter} from "./Helpers/table_helpers";
import {GraderDistributionModal} from "./Modals/graders_distribution_modal";
import {SectionDistributionModal} from "./Modals/section_distribution_modal";

class GradersManager extends React.Component {
constructor(props) {
Expand All @@ -22,6 +23,7 @@ class GradersManager extends React.Component {
hide_unassigned_criteria: false,
sections: {},
isGraderDistributionModalOpen: false,
isSectionDistributionModalOpen: false,
show_hidden: false,
show_hidden_groups: false,
hidden_graders_count: 0,
Expand Down Expand Up @@ -51,6 +53,11 @@ class GradersManager extends React.Component {
isGraderDistributionModalOpen: true,
});
};
openSectionDistributionModal = () => {
this.setState({
isSectionDistributionModalOpen: true,
});
};

fetchData = () => {
fetch(Routes.course_assignment_graders_path(this.props.course_id, this.props.assignment_id), {
Expand Down Expand Up @@ -87,6 +94,7 @@ class GradersManager extends React.Component {
anonymize_groups: res.anonymize_groups,
hide_unassigned_criteria: res.hide_unassigned_criteria,
isGraderDistributionModalOpen: false,
isSectionDistributionModalOpen: false,
hidden_graders_count: res.graders.filter(grader => grader.hidden).length,
inactive_groups_count: inactive_groups_count,
});
Expand Down Expand Up @@ -124,6 +132,25 @@ class GradersManager extends React.Component {
}).then(this.fetchData);
};

assignSections = assignments => {
let sections = Object.keys(assignments);
let graders = Object.values(assignments);
$.post({
url: Routes.global_actions_course_assignment_graders_path(
this.props.course_id,
this.props.assignment_id
),
data: {
global_actions: "assign_sections",
current_table: this.state.tableName,
skip_empty_submissions: this.state.skip_empty_submissions,
assignments: assignments,
sections: sections,
graders: graders,
},
}).then(this.fetchData);
};

assignRandomly = weightings => {
let groups = this.groupsTable ? this.groupsTable.state.selection : [];
let criteria = this.criteriaTable ? this.criteriaTable.state.selection : [];
Expand Down Expand Up @@ -305,6 +332,7 @@ class GradersManager extends React.Component {
<GradersActionBox
assignAll={this.assignAll}
openGraderDistributionModal={this.openGraderDistributionModal}
openSectionDistributionModal={this.openSectionDistributionModal}
unassignAll={this.unassignAll}
showHidden={this.state.show_hidden}
showHiddenGroups={this.state.show_hidden_groups}
Expand Down Expand Up @@ -406,6 +434,15 @@ class GradersManager extends React.Component {
onSubmit={this.assignRandomly}
/>
)}
{this.state.isSectionDistributionModalOpen && (
<SectionDistributionModal
isOpen={this.state.isSectionDistributionModalOpen}
onRequestClose={() => this.setState({isSectionDistributionModalOpen: false})}
onSubmit={this.assignSections}
graders={this.state.graders}
sections={this.state.sections}
/>
)}
</div>
);
}
Expand Down Expand Up @@ -778,6 +815,10 @@ class GradersActionBox extends React.Component {
<FontAwesomeIcon icon="fa-solid fa-dice" />
{I18n.t("graders.actions.randomly_assign_graders")}
</button>
<button onClick={this.props.openSectionDistributionModal}>
<FontAwesomeIcon icon="fa-solid fa-list" />
{I18n.t("graders.actions.assign_by_section")}
</button>
<button onClick={this.props.unassignAll}>
<FontAwesomeIcon icon="fa-solid fa-user-minus" />
{I18n.t("graders.actions.unassign_grader")}
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/fontawesome_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
faGripVertical,
faInfo,
faLink,
faList,
faMinus,
faPen,
faPeopleGroup,
Expand Down Expand Up @@ -87,6 +88,7 @@ library.add(
faGripVertical,
faInfo,
faLink,
faList,
faMinus,
faPen,
faPeopleGroup,
Expand Down
69 changes: 62 additions & 7 deletions app/controllers/graders_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ def grader_criteria_mapping
# These actions act on all currently selected graders & groups
def global_actions
@assignment = Assignment.find(params[:assignment_id])
grader_ids = params[:graders]
if grader_ids.blank?
grader_ids = params[:graders] || []
if grader_ids.blank? && params[:global_actions] != 'assign_sections'
grader_ids = current_course.tas.joins(:user).where('users.user_name': params[:grader_user_names]).ids
if grader_ids.blank?
flash_now(:error, I18n.t('graders.select_a_grader'))
Expand Down Expand Up @@ -106,11 +106,21 @@ def global_actions

case params[:current_table]
when 'groups_table'
grouping_ids = params[:groupings]
if grouping_ids.blank?
flash_now(:error, I18n.t('groups.select_a_group'))
head :bad_request
return
if params[:global_actions] == 'assign_sections'
assignments = params[:assignments]
if assignments.blank?
flash_now(:error, I18n.t('graders.select_a_grader'))
head :bad_request
return
end
grouping_hash = filter_grouping_by_section(assignments, @assignment)
else
grouping_ids = params[:groupings]
if grouping_ids.blank?
flash_now(:error, I18n.t('groups.select_a_group'))
head :bad_request
return
end
end

case params[:global_actions]
Expand Down Expand Up @@ -156,6 +166,33 @@ def global_actions
flash_now(:error, e.message)
return
end
when 'assign_sections'
found_empty_submission = false
filtered_grouping_hash = {}

grouping_hash.each do |ta_id, group_ids|
if params[:skip_empty_submissions] == 'true'
filtered_grouping_ids = filter_empty_submissions(group_ids)
if filtered_grouping_ids.count != group_ids.count
found_empty_submission = true
end
else
filtered_grouping_ids = group_ids
end
filtered_grouping_hash[ta_id] = filtered_grouping_ids
end

begin
if found_empty_submission
assign_graders_by_section(filtered_grouping_hash)
flash_now(:info, I18n.t('graders.group_submission_no_files'))
else
assign_graders_by_section(grouping_hash)
end
rescue StandardError => e
head :bad_request
flash_now(:error, e.message)
end
end
when 'criteria_table'
positions = params[:criteria]
Expand Down Expand Up @@ -205,6 +242,10 @@ def assign_all_graders(grouping_ids, grader_ids)
Grouping.assign_all_tas(grouping_ids, grader_ids, @assignment)
end

def assign_graders_by_section(grouping_hash)
Grouping.assign_by_section(grouping_hash, @assignment)
end

def unassign_graders(grouping_ids, grader_ids)
grader_membership_ids = TaMembership.where(grouping_id: grouping_ids, role_id: grader_ids).ids
Grouping.unassign_tas(grader_membership_ids, grouping_ids, @assignment)
Expand All @@ -218,6 +259,20 @@ def filter_empty_submissions(grouping_ids)
end
end

def filter_grouping_by_section(section_assignments, assignment)
ta_groupings = {}

section_assignments.each do |section_name, ta_id|
grouping_ids = assignment.groupings.joins(:section)
.where(section: { name: section_name })
.ids

ta_groupings[ta_id] = grouping_ids
end

ta_groupings
end

def implicit_authorization_target
OpenStruct.new policy_class: GraderPolicy
end
Expand Down
9 changes: 9 additions & 0 deletions app/models/grouping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ def self.assign_all_tas(grouping_ids, ta_ids, assignment)
end
end

def self.assign_by_section(groupings_by_ta, assignment)
groupings_by_ta.each do |ta_id, grouping_ids|
assign_tas(grouping_ids, [ta_id], assignment) do |grouping_ids_, ta_ids_|
# Return the pairs of grouping_ids and the single ta_id
grouping_ids_.product(ta_ids_)
end
end
end

# Assigns TAs to groupings using a caller-specified block. The block is given
# a list of grouping IDs and a list of TA IDs and must return a list of
# grouping-ID-TA-ID pair that represents the TA assignment.
Expand Down
3 changes: 3 additions & 0 deletions config/locales/views/graders/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
en:
graders:
actions:
assign_by_section: Assign graders by section
assign_grader: Assign grader(s)
randomly_assign_graders: Randomly assign grader(s)
unassign_grader: Unassign grader(s)
anonymize_groups: Hide group membership information from graders
assign_by_section_instruction: Select a grader to assign to each section. The same grader can be assigned to multiple sections. Leave a section blank if no grader should be assigned.
assign_by_section_modal_title: Assign Graders by Section
assign_to_criteria: Assign graders to individual criteria (if unselected, all graders will be able to edit all criteria)
assigned_graders: Assigned Graders
coverage: Coverage
Expand Down
Loading

0 comments on commit 0a03ff5

Please sign in to comment.