Canvas All Info

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

// ==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();