For Developers
Using a Rubric to Grade an Assignment
Just want the code?
This example was built and tested on an integration using Canvas as the underlying source. Other LMS providers may differ slightly in what is supported.
Goal
Get an Assignment's Rubric, and use that to grade a Student's Submission.
Why
Teachers often want to use Rubrics for grading, and this serves as an example of how Edlink can facilitate that.
How
Before this script will work, you'll need a few things:
- Your valid Graph API token
- A valid set of class + assignment + submission UUIDs (make sure the Assignment you use was created with a rubric attached)
This script will use that information to:
- Call the Get Assignment endpoint to get the
rubric_id
. - Call the Get Rubric endpoint to get the rubric's
performance_level
options. - Get the user's choices for the student's grade.
- Call the Update Submission endpoint to grade the Submission.
Save the following script to grade-a-rubric.js
// IMPORTANT TODO - you must replace these ids with valid ids for your integration
const CLASS_ID = '00000000-0000-0000-0000-000000000000';
const ASSIGNMENT_ID = '00000000-0000-0000-0000-000000000000';
const SUBMISSION_ID = '00000000-0000-0000-0000-000000000000';
const EDLINK_BASE_URL = 'https://ed.link/api/v2/graph';
const access_token = process.argv[2];
if (!access_token) {
console.error('access token must be provided as command line argument');
process.exit(1);
}
// helper function for Edlink API calls
const fetchWithToken = async (url) => {
if (url.startsWith('/')) {
url = `${EDLINK_BASE_URL}${url}`;
}
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${access_token}`
}
});
if (!response.ok) {
console.error(url, response);
throw new Error(`HTTP error on ${url} Status: ${response.status}`);
}
return await response.json();
};
// helper class for fancy display
class InteractiveMenu {
constructor(title, subtitle, options) {
this.title = title;
this.subtitle = subtitle;
this.options = options;
this.selectedIndex = 0;
this.isActive = false;
}
// ANSI escape codes for terminal control
static ANSI = {
CLEAR_SCREEN: '\x1B[2J\x1B[0f',
HIDE_CURSOR: '\x1B[?25l',
SHOW_CURSOR: '\x1B[?25h',
RESET: '\x1B[0m',
DIM: '\x1B[2m',
REVERSE: '\x1B[7m',
GREEN: '\x1B[32m',
YELLOW: '\x1B[33m',
CYAN: '\x1B[36m'
};
render() {
const { CLEAR_SCREEN, HIDE_CURSOR, DIM, REVERSE, CYAN, GREEN, YELLOW, RESET } = InteractiveMenu.ANSI;
// Clear screen and hide cursor
process.stdout.write(CLEAR_SCREEN + HIDE_CURSOR);
// Display title
console.log(`${CYAN}╔${'═'.repeat(this.title.length + 2)}╗${RESET}`);
console.log(`${CYAN}║ ${this.title} ║${RESET}`);
console.log(`${CYAN}╚${'═'.repeat(this.title.length + 2)}╝${RESET}\n`);
// Display subtitle
console.log(`${YELLOW}╔${'═'.repeat(this.subtitle.length + 2)}╗${RESET}`);
console.log(`${YELLOW}║ ${this.subtitle} ║${RESET}`);
console.log(`${YELLOW}╚${'═'.repeat(this.subtitle.length + 2)}╝${RESET}\n`);
// Display options
this.options.forEach((option, index) => {
const isSelected = index === this.selectedIndex;
const prefix = isSelected ? '▶ ' : ' ';
const style = isSelected ? `${REVERSE}${GREEN}` : `${DIM}`;
const icon = option.icon || '●';
console.log(`${style}${prefix}${icon} ${option.text}${RESET}`);
});
// Display instructions
console.log(`\n${DIM}Use ↑/↓ arrow keys to navigate, Enter to select, 'q' to quit${RESET}`);
}
moveUp() {
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.options.length - 1;
this.render();
}
moveDown() {
this.selectedIndex = this.selectedIndex < this.options.length - 1 ? this.selectedIndex + 1 : 0;
this.render();
}
select() {
const selectedOption = this.options[this.selectedIndex];
this.cleanup();
if (selectedOption.result) {
return selectedOption.result;
}
return selectedOption;
}
cleanup() {
const { SHOW_CURSOR, CLEAR_SCREEN } = InteractiveMenu.ANSI;
process.stdout.write(CLEAR_SCREEN + SHOW_CURSOR);
process.stdin.setRawMode(false);
this.isActive = false;
}
async show() {
return new Promise((resolve, reject) => {
this.isActive = true;
// Set up raw mode to capture individual keystrokes
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
// Initial render
this.render();
// Handle keyboard input
const keyHandler = (key) => {
if (!this.isActive) return;
switch (key) {
case '\u001B[A': // Up arrow
this.moveUp();
break;
case '\u001B[B': // Down arrow
this.moveDown();
break;
case '\r': // Enter
const selected = this.select();
process.stdin.removeListener('data', keyHandler);
this.cleanup();
resolve(selected);
break;
case 'q':
case 'Q':
case '\u0003': // Ctrl+C
process.stdin.removeListener('data', keyHandler);
this.cleanup();
resolve(null);
break;
default:
break; // Ignore other keys
}
};
process.stdin.on('data', keyHandler);
// Handle process termination
process.on('SIGINT', () => {
process.stdin.removeListener('data', keyHandler);
this.cleanup();
process.exit(0);
});
});
}
}
/// Usage Example ///
// 1. GET the Assignment from Edlink
const assignment_response = await fetchWithToken(`/classes/${CLASS_ID}/assignments/${ASSIGNMENT_ID}`);
const assignment = assignment_response['$data'];
const rubric_id = assignment.rubric_id;
// 2. GET the Rubric from Edlink
const rubric_response = await fetchWithToken(`/classes/${CLASS_ID}/rubrics/${rubric_id}`);
const rubric = rubric_response['$data'];
// 3. Ask the user what grade to give the submission
let rubric_grade = {};
for (const criteria of rubric.grading_criteria) {
const menu = new InteractiveMenu(
`${rubric.title} - select student performance`,
`${criteria.name} - ${criteria.description}`,
criteria.performance_levels.map((l) => ({
text: [l.name, l.description, `${l.points} points`].filter((a) => a).join(' - '),
result: l.name
}))
);
const choice = await menu.show();
if (!choice) {
console.error('no choice selected');
process.exit(1);
}
rubric_grade[criteria.name] = choice;
}
// 4. PATCH the Submission in Edlink (using the grading choices the user made)
console.log('Your grading selections are:', rubric_grade);
const submission_response = await fetch(`${EDLINK_BASE_URL}/classes/${CLASS_ID}/assignments/${ASSIGNMENT_ID}/submissions/${SUBMISSION_ID}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
rubric_grade
})
});
// and... we're done
if (submission_response.ok) {
console.log(`Submission ${SUBMISSION_ID} was graded. The Edlink response:`, await submission_response.json());
} else {
console.error(`Something went wrong`, submission_response, await submission_response.text());
}
process.exit(0);
Now, running node grade-a-rubric.js MY_INTEGRATION_ACCESS_TOKEN
will run the script and cause the specified Submission to be graded utilizing the rubric which attaches to the Assignment.