Canvas All Info

Place all information on a single page (https://canvas.example.com/all or https://example.instructure.com/all)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Canvas All Info
// @namespace    https://theusaf.github.io
// @version      1.4.0
// @icon         https://canvas.instructure.com/favicon.ico
// @copyright    2020-2021, Daniel Lau
// @license      MIT
// @description  Place all information on a single page (https://canvas.example.com/all or https://example.instructure.com/all)
// @author       theusaf
// @include      /^https:\/\/canvas\.[a-z0-9]*?\.[a-z]*?\/all\/?(\?.*)?$/
// @include      /^https:\/\/[a-z0-9]*?\.instructure\.com\/all\/?(\?.*)?$/
// @inject-into  page
// @grant        none
// ==/UserScript==

/*
  Note:
  - This userscript uses public APIs accessed by canvas
    - Gets class information
    - Gets assignments
    - Gets basic teacher information
  - This userscript does not store or upload any information gathered by the script
  - This userscript overwrites /all in canvas
  - This userscript was originally developed for Oregon State University
*/

/* Useful Links (for use later?)
/api/v1/conversations?scope=inbox&filter_mode=and&include_private_conversation_enrollments=false
- Canvas mail
/api/v1/conversations/(mailbox_id)?include_participant_contexts=false&include_private_conversation_enrollments=false
- Specific mail
/courses/(class_id)/modules/items/assignment_info
- Module items
*/

/**
 * log - logs info to console
 * @param {*} str
 * @param  {...any} data
 */
const log = (str, ...data) => {
  if (data.length > 0) {
    console.log(`[CANVAS-ALL] ${str}`, data);
  }
  console.log(`[CANVAS-ALL] ${str}`);
};

/**
 * load - Loads everything
 */
async function load() {
  document.title = "Dashboard - All";

  /**
   * mainElement - the main application div
   * iFrameLoader - The div for loading iframes for getting data
   * styles - A style element
   */
  const mainElement = document.getElementById("main"),
    iFrameLoader = document.createElement("div");
  mainElement.innerHTML = `<style>
    #canvas-all-iframe-loader{
      visibility: hidden;
      position: fixed;
      width: 100%;
      height: 100%;
      pointer-events: none;
      left: 0;
    }
    #canvas-all-iframe-loader > iframe{
      width: 100%;
      height: 100%;
      position: absolute;
      left: 0;
      top: 0;
    }
    #main{
      display: flex;
      flex-flow: column;
      padding: 1rem;
    }
    #main>span{
      flex: 0;
      margin-bottom: 1rem;
    }
    #main>div{
      flex: 1;
    }
    #canvasall_main_wrapper{
      display: flex;
    }
    #canvasall_main_wrapper>div{
      flex: 75%;
    }
    #canvasall_class_grades{
      display: flex;
      flex-flow: column;
      background: #fff5e0;
      padding: 0.5rem;
      border-radius: 0.5rem;
    }
    #canvasall_assignments_wrapper{
      padding: 0.5rem;
      background: #fff5e0;
      border-radius: 0.5rem;
    }
    #canvasall_main_wrapper>#canvasall_announcement_wrapper{
      flex: 25%;
      padding: 0.5rem;
    }
    #canvasall_assignment_filter_list_chosen{
      display: inline-block;
    }
    #canvasall_assignment_filter_list_chosen>option{
      display: inline-block;
      background: grey;
      color: white;
      padding: 0.25rem;
      margin: 0.25rem;
      cursor: pointer;
    }
    #canvasall_assignment_filter_list_chosen>option::before{
      content: "x ";
    }
    .canvasall_announcement_wrapper{
      margin-bottom: 0.5rem;
      padding: 0.5rem;
      border-radius: 0.5rem;
    }
    .canvasall_announcement_wrapper:nth-child(2n+1){
      background: #fff5e0;
    }
    .canvasall_announcement_wrapper:nth-child(2n){
      background: #ddd;
    }
    .canvasall_announcement_title{
      font-size: 1.25rem;
      font-weight: bold;
    }
    .canvasall_announcement_class,
    .canvasall_announcement_when{
      font-size: 0.75rem;
    }
    .canvasall_announcement_class>a{
      color: grey;
    }
    .canvasall_class_grade_wrapper:nth-child(2n+1){
      background: white;
    }
    .canvasall_class_grade_wrapper:nth-child(2n){
      background: #eee;
    }
    .canvasall_class_grade_wrapper{
      flex: 1;
      display: flex;
      border-radius: 0.5rem;
    }
    .canvasall_class_grade_wrapper>div{
      flex: 1;
      padding: 0.5rem;
      word-break: break-all;
    }
    .canvasall_assignment_wrapper:nth-child(2n+1){
      background: rgba(255,255,255,0.8);
    }
    .canvasall_assignment_wrapper:nth-child(2n){
      background: rgba(255,255,255,0.4);
    }
    .canvasall_assignment_wrapper{
      display: flex;
      padding: 0.5rem;
      border-radius: 0.5rem;
    }
    .canvasall_assignment_wrapper>div{
      flex: 1;
      padding-right: 0.25rem;
      padding-left: 0.25rem;
    }
    .canvasall_assignment_title{
      display: flex;
      align-items: center;
    }
    .canvasall_assignment_title>img{
      height: 1.5rem;
      width: 1.5rem;
      margin-right: 0.5rem;
    }
    .canvasall_assignment_icon {
      min-height: 1.5rem;
      min-width: 1.5rem;
      margin-right: 0.5rem;
    }
    .canvasall_assignment_type_assignment:before {
      content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDI4MCAyNTkiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI4MCAyNTkiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik03My4zMSwxOThjLTExLjkzLDAtMjIuMjIsOC0yNCwxOC43M2EyNi42NywyNi42NywwLDAsMC0uMywzLjYzdi4zYTIyLDIyLDAsMCwwLDUuNDQsMTQuNjUsMjIuNDcsMjIuNDcsMCwwLDAsMTcuMjIsOEgyMDBWMjI4LjE5aC0xMzRWMjEzLjA4SDIwMFYxOThabTIxLTEwNS43NGg5MC42NFY2Mkg5NC4zWk03OS4xOSwxMDcuMzRWNDYuOTJIMjAwdjYwLjQyWm03LjU1LDMwLjIxVjEyMi40NUgxOTIuNDl2MTUuMTFaTTcxLjY1LDE2LjcxQTIyLjcyLDIyLjcyLDAsMCwwLDQ5LDM5LjM2VjE5MC44OGE0MS4xMiw0MS4xMiwwLDAsMSwyNC4zMi04aDE1N1YxNi43MVpNMzMuODgsMzkuMzZBMzcuNzgsMzcuNzgsMCwwLDEsNzEuNjUsMS42SDI0NS4zNlYxOThIMjE1LjE1djQ1LjMyaDIyLjY2VjI1OC40SDcxLjY1YTM3Ljg1LDM3Ljg1LDAsMCwxLTM3Ljc2LTM3Ljc2WiI+PC9wYXRoPjwvc3ZnPgo=");
    }
    .canvasall_assignment_type_quiz:before {
      content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDI4MCAyODAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI4MCAyODAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik05MS43MiwxMjAuNzVoOTYuNTZWMTA0LjY1SDkxLjcyWm0wLDQ4LjI4aDgwLjQ3VjE1Mi45NEg5MS43MlptMC05Ni41Nmg4MC40N1Y1Ni4zN0g5MS43MlptMTYwLjk0LDM0Ljg4SDIyOC41MlYxMC43OGgtMTc3djk2LjU2SDI3LjM0QTI0LjE3LDI0LjE3LDAsMCwwLDMuMiwxMzEuNDhWMjQ0LjE0YTI0LjE3LDI0LjE3LDAsMCwwLDI0LjE0LDI0LjE0SDI1Mi42NmEyNC4xNywyNC4xNywwLDAsMCwyNC4xNC0yNC4xNFYxMzEuNDhBMjQuMTcsMjQuMTcsMCwwLDAsMjUyLjY2LDEwNy4zNFptMCwxNi4wOWE4LjA2LDguMDYsMCwwLDEsOCw4djUxLjc3bC0zMi4xOSwxOS4zMVYxMjMuNDRaTTY3LjU4LDIwMy45MXYtMTc3SDIxMi40MnYxNzdaTTI3LjM0LDEyMy40NEg1MS40OHY3OS4xM0wxOS4yOSwxODMuMjZWMTMxLjQ4QTguMDYsOC4wNiwwLDAsMSwyNy4zNCwxMjMuNDRaTTI1Mi42NiwyNTIuMTlIMjcuMzRhOC4wNiw4LjA2LDAsMCwxLTgtOFYyMDJsMzAsMThIMjMwLjc1bDMwLTE4djQyLjEyQTguMDYsOC4wNiwwLDAsMSwyNTIuNjYsMjUyLjE5WiI+PC9wYXRoPjwvc3ZnPgo=");
    }
    .canvasall_assignment_type_discussion_topic:before {
      content: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgcm90YXRlPSIwIiBzdHlsZT0iaGVpZ2h0OjFlbTt3aWR0aDoxZW0iIHZpZXdCb3g9IjAgMCAxOTIwIDE5MjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnPgo8cGF0aCBkPSJtNjc3LjY1IDE2djMzOC45NGgxMTIuOTR2LTIyNS44OGgxMDE2LjV2NzkwLjQ4aC0yMjZ2MjU5Ljc2bC0yNTkuNjUtMjU5Ljc2aC03OS4xNzJ2LTQ1MS42NWgtNTY0LjU5LTY3Ny42NXYxMDE2LjVoMzM4LjcxdjQxOC45bDQxOC00MTguOWg0ODUuNTN2LTQ1MS44OGgzMi43NTNsNDE5LjEyIDQxOS4xMnYtNDE5LjEyaDIyNS44OHYtMTAxNi41aC0xMjQyLjR6bS0zMzguODYgOTAzLjU2aDU2NC43MXYtMTEyLjk0aC01NjQuNzF2MTEyLjk0em0wIDIyNS44OGgzMzguOTR2LTExMy4wNWgtMzM4Ljk0djExMy4wNXptLTIyNS44NS01NjQuNzRoMTAxNi41djc5MC43aC00MTkuMDFsLTI1OC43NSAyNTkuNjV2LTI1OS42NWgtMzM4Ljcxdi03OTAuN3oiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgo8L2c+Cjwvc3ZnPgo=")
    }
    .canvasall_status_submit,
    .canvasall_status_submit a{
      color: green;
    }
    .canvasall_status_done{
      text-decoration: line-through;
    }
    .canvasall_status_late{
      background: orange !important;
      color: white;
    }
    .canvasall_status_late a{
      color: white;
    }
    .canvasall_status_missing{
      background: red !important;
      color: white;
    }
    .canvasall_status_missing a{
      color: white;
    }
    .canvasall_status_feedback>.canvasall_assignment_title::after{
      content: " (Feedback available)"
    }
  </style>
  <span id="canvasall_fetching_information">Fetching information... please wait...</span>
  <div id="canvasall_main_wrapper">
    <div>
      <div id="canvasall_class_wrapper">
        <!-- Courses, Grades, etc. -->
        <h3>Courses</h3>
        <div id="canvasall_class_grades">
          <div id="canvasall_class_grade_header" class="canvasall_class_grade_wrapper">
            <div><span>Class</span></div>
            <div><span>Grade</span></div>
            <div><span>Professor</span></div>
          </div>
        </div>
      </div>
      <h3>Current Assignments</h3>
      <div>
        <span>Filters:</span>
        <select id="canvasall_assignment_filter_status">
          <option value="">Select</option>
          <option value="submit">Hide Submitted</option>
          <option value="done">Hide Graded</option>
          <option value="late">Hide Late</option>
          <option value="missing">Hide Missing</option>
          <option value="quiz">Hide Quiz</option>
          <option value="assignment">Hide Assignment</option>
        </select>
        <div id="canvasall_assignment_filter_list_chosen">
        </div>
      </div>
      <div id="canvasall_assignments_wrapper">
        <div class="canvasall_assignment_wrapper">
          <div>
            <span>Assignment Name</span>
          </div>
          <div>
            <span>Class</span>
          </div>
          <div>
            <span>Due Date</span>
          </div>
        </div>
      </div>
    </div>
    <div id="canvasall_announcement_wrapper">
      <h3>Announcements</h3>
    </div>
  </div>`;
  iFrameLoader.id = "canvas-all-iframe-loader";
  mainElement.append(iFrameLoader);

  /**
   * courses - Class information
   * classAssignments - Assignments for courses
   * mainFrame - The main iframe
   * ENV - Global variables with useful data
   * currentUserID - The current user id
   */
  log("Getting courses");
  const courses = await getCourses(),
    courseAssignments = [],
    courseGrades = {},
    { ENV } = window,
    { currentUserID } = ENV,
    courseGradeDiv = mainElement.querySelector("#canvasall_class_grades"),
    assignmentsDiv = mainElement.querySelector("#canvasall_assignments_wrapper"),
    announcementsDiv = mainElement.querySelector(
      "#canvasall_announcement_wrapper"
    ),
    statusFilter = mainElement.querySelector(
      "#canvasall_assignment_filter_status"
    ),
    statusFilterList = mainElement.querySelector(
      "#canvasall_assignment_filter_list_chosen"
    ),
    localStorageConfigStr = localStorage.canvasAllConfig ?? "{}",
    localStorageConfig = JSON.parse(localStorageConfigStr);

  function hideType(value) {
    console.log(`Hiding '${value}'`);
    if (value === "") {
      // ignore reset to empty value
      console.log("Ignoring empty value");
      return;
    }
    const elem = statusFilter.querySelector(
        `option[value="${value}"]`
      ),
      temp = document.createElement("template"),
      now = `${Math.random()}`.substring(2);
    if (!elem) {
      console.error("Could not find element");
      return;
    }
    temp.innerHTML = `<style id="canvasall_filter_${now}">
      .canvasall_status_${value}{
        display: none;
      }
    </style>`;
    localStorageConfig[value] = true;
    localStorage.canvasAllConfig = JSON.stringify(localStorageConfig);
    setTimeout(() => {
      document.body.append(temp.content.cloneNode(true));
      statusFilterList.append(elem);
      elem.addEventListener("click", click);
      statusFilter.value = "";
    });
    function click() {
      localStorageConfig[value] = false;
      localStorage.canvasAllConfig = JSON.stringify(localStorageConfig);
      // remove style
      document.querySelector(`#canvasall_filter_${now}`).remove();
      elem.removeEventListener("click", click);
      setTimeout(() => {
        statusFilter.append(elem);
        statusFilter.value = "";
      });
    }
  }

  // Filters
  statusFilter.addEventListener("change", () => {
    hideType(statusFilter.value);
  });

  // Begin writing class information
  for (const [i, course] of Object.entries(courses)) {
    const template = document.createElement("template");
    template.innerHTML = `<div class="canvasall_class_grade_wrapper" canvasall-class-id="${
      course.id
    }">
      <div class="canvasall_class_grade_name">
        <span>${+i + 1}. <a href="/courses/${course.id}">${
      course.name
    }</a></span>
      </div>
      <div class="canvasall_class_grade_score">
        <span>(Loading scores...)</span>
      </div>
      <div class="canvasall_class_grade_instructor">
        <span>(Loading instructors...)</span>
      </div>
    </div>`;
    courseGradeDiv.append(template.content.cloneNode(true));
  }

  log("Getting course assignments and grades");
  let courseCount = 0,
    finishedCollections = 0;
  for (const course of courses) {
    courseCount++;
    getCourseAssignments(course.id, currentUserID).then(assignments => {
      courseAssignments.push.apply(
        courseAssignments,
        assignments
      );
      finishedCollections++;
      if (finishedCollections === courseCount) {
        // Write assignments
        courseAssignments.sort((a, b) => {
          // sort by "due date"
          // also places non-due-date at end. (+1 week)
          const dueA = new Date(
              a.plannable.due_at || b.plannable.created_at || Date.now() + 604800e3
            ),
            dueB = new Date(
              b.plannable.due_at || b.plannable.created_at || Date.now() + 604800e3
            );
          return dueA.getTime() - dueB.getTime();
        });

        log("Compiling assignments");
        for (const assignment of courseAssignments) {
          const template = document.createElement("template");
          if (assignment.plannable_type === "announcement") {
            // Put in announcement thing
            template.innerHTML = `<div class="canvasall_announcement_wrapper">
                <div class="canvasall_announcement_title">
                  <a href="${assignment.html_url}">${assignment.plannable.title}</a>
                </div>
                <div class="canvasall_announcement_class">
                  <a href="/courses/${assignment.course_id}">${
              assignment.context_name
            }</a>
                </div>
                <div class="canvasall_announcement_when">
                  <span>${new Date(assignment.plannable.created_at)
                    .toString()
                    .split(" ")
                    .slice(0, 5)
                    .join(" ")
                    .replace(/:\d{2}$/, "")
                    .replace(/\s(?=\w{3}\s\d{2})/, ", ")
                    .replace(/\d{4}/, "at")}</span>
                </div>
              </div>`;
            announcementsDiv.append(template.content.cloneNode(true));
            continue;
          }
          const divClasses = [],
            { submissions } = assignment;
          if (submissions) {
            const { excused, graded, has_feedback, late, missing, submitted } =
              submissions;
            if (excused || graded) {
              divClasses.push("canvasall_status_done");
            }
            if (submitted) {
              divClasses.push("canvasall_status_submit");
            }
            if (late) {
              divClasses.push("canvasall_status_late");
            }
            if (missing) {
              divClasses.push("canvasall_status_missing");
            }
            if (has_feedback) {
              divClasses.push("canvasall_status_feedback");
            }
          }

          divClasses.push("canvasall_status_" + assignment.plannable_type);
          template.innerHTML = `<div class="canvasall_assignment_wrapper ${divClasses.join(
            " "
          )}" class-id="${assignment.course_id}">
            <div class="canvasall_assignment_title">
              <div class="canvasall_assignment_icon canvasall_assignment_type_${
                assignment.plannable_type
              }"></div>
              <a href="${assignment.html_url}">${assignment.plannable.title}</a>
            </div>
            <div class="canvasall_assignment_class">
              <a href="/courses/${assignment.course_id}">${
            assignment.context_name
          }</a>
            </div>
            <div class="canvasall_assignment_due">
              <span>${
                assignment.plannable.due_at
                  ? new Date(assignment.plannable.due_at)
                      .toString()
                      .split(" ")
                      .slice(0, 5)
                      .join(" ")
                      .replace(/:\d{2}$/, "")
                      .replace(/\s(?=\w{3}\s\d{2})/, ", ")
                      .replace(/\d{4}/, "at")
                  : "No due date"
              }</span>
            </div>
          </div>`;
          assignmentsDiv.append(template.content.cloneNode(true));
        }

        // Restore hide
        for (const [key, value] of Object.entries(localStorageConfig)) {
          if (value && key !== "") {
            hideType(key);
          }
        }

        courseAssignments.splice(0); // Free up memory
        document.querySelector("#canvasall_fetching_information").outerHTML = "";
      }
    });
    // Get grades
    loadFrame(iFrameLoader, `/courses/${course.id}/grades`).then(
      (ClassGradeFrame) => {
        const { document: d, window: w, frame } = ClassGradeFrame;
        let overallGrade = d
            .querySelector(
              "#student-grades-right-content .student_assignment.final_grade > .grade"
            )
            ?.textContent?.replace(/%|\s/g, ""),
          titles = d.querySelectorAll(".title"),
          possiblePoints = d.querySelectorAll(".points_possible"),
          dueDates = d.querySelectorAll(".due");
        courseGrades[course.id] = {
          Grades: overallGrade + "%",
          Titles: titles,
          PossiblePoints: possiblePoints,
          DueDates: dueDates,
        };
        // Write grades
        const gradeDiv = courseGradeDiv.querySelector(
            `[canvasall-class-id="${course.id}"] > .canvasall_class_grade_score`
          ),
          { ENV } = w,
          { grading_scheme } = ENV,
          [letter] =
            (grading_scheme ?? ["N/A", 0]).find((scheme) => {
              try {
                return +overallGrade / 100 >= scheme[1];
              } catch (e) {
                console.error(e);
              }
            }) ?? ["?"];
        gradeDiv.innerHTML =
          overallGrade !== "N/A" && overallGrade
            ? `<span>${letter} (${overallGrade}%)</span>`
            : `<span>N/A</span>`;
        // Done using data from iframe, attempt to clean up memory usage
        overallGrade = titles = possiblePoints = dueDates = null;
        for (const i in courseGrades) {
          delete courseGrades[i];
        }
        w.location = "about:blank";
        setTimeout(() => {
          frame.remove();
        }, 500);
      }
    );
    // Get instructors
    getCourseTeacher(course.id)
      .then((teacher) => {
        const teacherDiv = courseGradeDiv.querySelector(
          `[canvasall-class-id="${course.id}"] > .canvasall_class_grade_instructor`
        );
        teacherDiv.innerHTML = `<span>${teacher.name}</span>`;
      })
      .catch(() => {
        // no teacher, or failed to get teacher
        const teacherDiv = courseGradeDiv.querySelector(
          `[canvasall-class-id="${course.id}"] > .canvasall_class_grade_instructor`
        );
        teacherDiv.innerHTML = "<span>No instructor found.</span>";
      });
  }
}

// /api/v1/users/:user_id/communication_channels
/**
 * getCourses - Gets all the classes of the user
 *
 * @returns {Promise<Array>} A list of class information
 */
function getCourses() {
  return new Promise((res, rej) => {
    const x = new XMLHttpRequest();
    x.open(
      "GET",
      "/api/v1/users/self/favorites/courses?include[]=term&exclude[]=enrollments&sort=nickname"
    );
    x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    x.setRequestHeader(
      "accept",
      "application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01"
    );
    x.onload = () => {
      res(JSON.parse(x.responseText));
    };
    x.onerror = () => {
      rej();
    };
    x.send();
  });
}

/**
 * GetClassAssignments - Gets the class assignments
 *
 * @param  {String} courseID The class id
 * @param  {String} userID The user id
 * @returns {Promise<Array>} The list of assignments
 */
function getCourseAssignments(courseID, userID) {
  const x = new XMLHttpRequest(),
    now = new Date(),
    offset = now.getTimezoneOffset() / 60,
    nextUrlRegex = /[^<>]*(?=>; rel="next")/;
  now.addDays(-14); // To get assignments from 2 weeks ago
  now.setHours(8 - offset);
  now.setMinutes(0);
  now.setSeconds(0);
  now.setMilliseconds(0);

  function makeHttpRequest(url) {
    return new Promise((res, rej) => {
      x.open("GET", url);
      x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
      x.setRequestHeader(
        "accept",
        "application/json+canvas-string-ids, application/json+canvas-string-ids, application/json, text/plain, */*"
      );
      x.onerror = () => {rej();}
      x.onload = () => {
        res(x);
      }
      x.send();
    });
  }

  async function gatherRecursiveRequests(url, list = []) {
    const response = await makeHttpRequest(url),
      data = JSON.parse(response.responseText),
      linkHeader = response.getResponseHeader("link");
    list.push(...data);
    if (linkHeader) {
      const lastLink = linkHeader.match(nextUrlRegex);
      if (lastLink) {
        return gatherRecursiveRequests(lastLink[0], list);
      } else {
        return list;
      }
    } else {
      return list;
    }
  }

  return gatherRecursiveRequests(`/api/v1/planner/items?start_date=${now.toISOString()}&order=asc&context_codes[]=course_${courseID}`) //&context_codes[]=user_${userID}
    .catch(() => []);
}

/**
 * getCourseTeacher - Gets the teacher of the course
 *
 * @param  {String} courseID The class id
 * @returns {Object} The teacher
 */
function getCourseTeacher(courseID) {
  return new Promise((res, rej) => {
    const x = new XMLHttpRequest();
    x.open(
      "GET",
      `/api/v1/search/recipients?search=&per_page=20&permissions[]=send_messages_all&messageable_only=true&synthetic_contexts=true&context=course_${courseID}_teachers`
    );
    x.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    x.setRequestHeader(
      "Accept",
      "application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01"
    );
    x.onload = () => {
      res(JSON.parse(x.responseText)[0]);
    };
    x.onerror = () => {
      rej();
    };
    x.send();
  });
}

/**
 * loadFrame - Loads an iframe
 *
 * @param  {HTMLElement} iFrameLoader The element to append iframes to
 * @param  {String} src The iframe url to load
 * @returns {Promise<Object>} returns an object with the "window" and "document" of the iframe
 */
function loadFrame(iFrameLoader, src) {
  return new Promise((res) => {
    const mainFrame = document.createElement("iframe");
    mainFrame.src = src || "/";
    iFrameLoader.append(mainFrame);
    mainFrame.addEventListener("load", () => {
      const frameContext = {
        document: mainFrame.contentDocument,
        window: mainFrame.contentWindow,
        frame: mainFrame
      };
      res(frameContext);
    });
  });
}

load();