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 Promises 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:

  1. Authenticating the teacher
  2. Getting the data from each course
  3. 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.

Subscribe to Ori Marash

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe