Creating an analysis tool for department heads
In my school, we use a tool called "Canvas" to manage the homework,
tests, lessons, etc. in school. Canvas is open-sourced (all the code is on
GitHub), so editing the code is quite easy. However, since my school opted for a
hosted solution rather than a self-hosted one, we can't edit the code
(π).
But, since there's an API, and you can add in custom javascript to the frontend, you can add in whatever code you want. Of course, this is a very hacky solution, but it works!
Task 1 - Replacing the title
The default title for the login page of the website was "Log in to Canvas".
There didn't seem to be an option to change it, so we had to hack it! Just some
simple code to edit the title through the custom javascript and it worked!
if (document.title === 'Log in to canvas') {
document.title = 'King Alfred School Canvas';
}
At this point, I was hungry for more work...
Task 2 - Adding a dropdown for the calendar
There is a calendar feature in Canvas where a student can see their homework,
and parents/guardians can see their children's homework. However, if a parent
has more than one child in the school, they can't see individual children's
homework!
This is easy enough to change since if you change the URL:
/calendar --> /calendar?include_contexts={courses}
You can view just the courses listed in the include_contexts
value.
Great! All we have to do now is find the courses of each student, then change
the url depending on which child is chosen. Here's the code:
function addChooseChild() {
$.ajax({
method: 'get',
url: '/api/v1/users/self/observees?per_page=50',
dataType: 'json'
}).then(children => {
var p = [];
for (let child of children) {
p.push(new Promise(function(resolve, reject) {
$.ajax({
method: 'get',
url: '/api/v1/users/' + child.id + '/courses?per_page=50&include[]=term',
dataType: 'json'
}).then(courses => {
var listCourses = [];
for (let course of courses) {
// If course is active
if (course.term && (Date.now()) > Date.parse(course.term.start_at) && (Date.now()) < Date.parse(course.term.end_at)) {
listCourses.push('course_' + course.id)
}
}
resolve({
name: child.name,
courses: listCourses
});
}).fail(err => {
reject(err);
});
}));
}
Promise.all(p).then(res => {
$('#calendar_header').append(`
<select id="calendar_children" onchange="location = this.value;">
<option class="calendar_child">Select Child</option>
${
res.map(
x => `<option class="calendar_child" value="calendar?include_contexts=${x.courses}">${x.name}</option>`
)
}
</select>
`)
})
}).fail(err => {
throw err;
})
};
// If on the calendar page
if (window.location.href.indexOf('calendar')) {
addChooseChild();
}
This code took some time to write because I didn't know how Promise
s work. I
had to read a book called ES6 and Beyond by Kyle Simpson.
Task 3 - Head of department reports
After I showed I could handle this work, my next task was to create a website
where department heads could make sure the teachers in their department were
setting a reasonable amount of homework, and not forgetting to assign it on
Canvas.
This had a few challenges:
- Authenticating the teacher
- Getting the data from each course
- Presenting it
Authentication
To make sure only teachers in my school could view the data, I used
Canvas's OAuth2. This step was simple in retrospect, but this was the first time
I had created a system like this, so it took some time.
I learned a lot about how OAuth2 works in real life (not just the theory) and
made sure to follow the security guidelines.
Fetching data
Next was getting the data for the course. For this, I build an npm
package (canvas-api-helper) which lets you easily query the
Canvas API.
I used create-react-app to bootstrap a React project which would present the
assignments. Making the query on the client-side took a ridiculous amount of
time (10-20 seconds!) so I needed a way to cache the results.
The way I thought would work best is through Firestore. I would run a script
which gets data from Canvas and save it in Firestore, then query the data from
there to get it much faster.
I wrote a Firebase Cloud Function which would run this process every hour.
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
const needle = require('needle')
const options = {
headers: {
'Authorization': 'Bearer ' + functions.config().canvas.token
}
}
async function getDepartments() {
let url = 'https://kingalfred.test.instructure.com/api/v1/accounts/1/sub_accounts?per_page=100'
var departments = await needle('get', url, options)
return departments.body
}
async function getCoursesInDepartment(id) {
// TODO: investigate per_page, it's limiting the number of courses returned
// for now, it's 10 so that tests can be made quickly
let url = 'https://kingalfred.test.instructure.com/api/v1/accounts/' + id + '/courses?include=teachers&per_page=100'
var courses = await needle('get', url, options)
return courses.body
}
async function getAssignmentsInCourse(id) {
let url = 'https://kingalfred.test.instructure.com/api/v1/courses/' + id + '/assignments?per_page=100'
var assignments = await needle('get', url, options)
return assignments.body
}
async function getYearOfCourse(id) {
let url = 'https://kingalfred.test.instructure.com/api/v1/courses/' + id
var course = await needle('get', url, options)
return course.name.match()
}
exports.sync = functions.https.onRequest(async (req, res) => {
var data = {}
// Get departments
var departments = await getDepartments()
// For each department...
await Promise.all(departments.map(async (department) => {
// Get all of it's courses
var courses = await getCoursesInDepartment(department.id)
// Save the courses (to allow for assignments to add themselves to their course)
data[department.id] = {
name: department.name,
departmentBody: department,
courses
}
// this complicated line of code basically means that each function will
// happen asynchronously
await Promise.all(courses.map(async (course) => {
// Get assignments in the course
var assignments = await getAssignmentsInCourse(course.id)
// find the correct course...
data[department.id].courses.find((x, i) => {
if (x.id === course.id) {
// ... and save the assignment to it
data[department.id].courses[i] = {
...x,
assignments: assignments
}
return true
}
})
}))
}))
// Now that we have a big object with all the departments, courses, assignments,
// etc. we should move all assignments from it to a single array wich we push
// to firestore
var assignments = []
for (var i of Object.keys(data)) { // for each department
for (var course of data[i].courses) { // for each course
for (var assignment of course.assignments) { // for each assignment
var yearGroup = course.name.match(/Year (\d{1,2})/g) // gets year group from a string in the format: Year <year group> Subject with Teacher
console.log(course.teachers)
assignments.push({
department: data[i].departmentBody.id,
departmentName: data[i].departmentBody.name,
course: course.id,
yearGroup: yearGroup !== null ? yearGroup[0] : '' ,
teachers: course.teachers,
date: new Date(),
data: assignment
})
}
}
}
var b = admin.firestore().batch()
var i = 0
for (var a of assignments) {
b.set(
admin.firestore().doc('assignments/' + a.data.id),
a
)
console.log(a.data.name)
// Batch 400 queries together, then restart the batch so that firestore doesn't get angry
if (i > 400) {
i = 0
await b.commit()
b = admin.firestore().batch()
}
i++
}
// Update lastChanged time
await admin.firestore().doc('other/meta')
.update({ lastChanged: new Date() })
// Send back the assignments as JSON, mainly for debugging purposes
res.send(assignments)
})
I made sure to document the code since when I leave school, someone else
might have to read and understand the code I've written.
Presentation
Presenting the data was quite easy, as I had to do was create an interface using
Bootstrap and React. I had done this before so it didn't take too long.
Hosting
To host this system, I uploaded the entire project to Firebase. Firebase has
cloud functions (so I can run the sync
function presented earlier) and it has
hosting so I could serve the React app. A perfect solution.
Conclusion
The main benefit I got from this project was learning how to work with a client.
My physis teacher, Peter, is in charge of Canvas in our school. He would send
emails, host meetings and give me feedback about his requests and my solutions.
A big part of web development is understanding the client's needs, so having
some experience in this is very beneficial.