Canvas All Info

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

Od 06.01.2021.. Pogledajte najnovija verzija.

// ==UserScript==
// @name         Canvas All Info
// @namespace    https://theusaf.github.io
// @version      1.2.4
// @icon         https://canvas.instructure.com/favicon.ico
// @copyright    2020-2021, Daniel Lau
// @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\/?(\?.*)?$/
// @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
*/

/**
 * Load - Loads everything
 */
async function Load(){

  document.title = "Dashboard - All";

  /**
   * Main - the main application div
   * iFrameLoader - The div for loading iframes for getting data
   * Styles - A style element
   */
  const Main = document.getElementById("main"),
    iFrameLoader = document.createElement("div");
  Main.innerHTML = `<style>
    #osu-all-iframe-loader{
      visibility: hidden;
      position: fixed;
      width: 100%;
      height: 100%;
      pointer-events: none;
      left: 0;
    }
    #osu-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;
    }
    #osuall_main_wrapper{
      display: flex;
    }
    #osuall_main_wrapper>div{
      flex: 75%;
    }
    #osuall_class_grades{
      display: flex;
      flex-flow: column;
      background: #fff5e0;
      padding: 0.5rem;
      border-radius: 0.5rem;
    }
    #osuall_assignments_wrapper{
      padding: 0.5rem;
      background: #fff5e0;
      border-radius: 0.5rem;
    }
    #osuall_main_wrapper>#osuall_announcement_wrapper{
      flex: 25%;
      padding: 0.5rem;
    }
    #osuall_assignment_filter_list_chosen{
      display: inline-block;
    }
    #osuall_assignment_filter_list_chosen>option{
      display: inline-block;
      background: grey;
      color: white;
      padding: 0.25rem;
      margin: 0.25rem;
      cursor: pointer;
    }
    #osuall_assignment_filter_list_chosen>option::before{
      content: "x ";
    }
    .osuall_announcement_wrapper{
      margin-bottom: 0.5rem;
      padding: 0.5rem;
      border-radius: 0.5rem;
    }
    .osuall_announcement_wrapper:nth-child(2n+1){
      background: #fff5e0;
    }
    .osuall_announcement_wrapper:nth-child(2n){
      background: #ddd;
    }
    .osuall_announcement_title{
      font-size: 1.25rem;
      font-weight: bold;
    }
    .osuall_announcement_class,
    .osuall_announcement_when{
      font-size: 0.75rem;
    }
    .osuall_announcement_class>a{
      color: grey;
    }
    .osuall_class_grade_wrapper:nth-child(2n+1){
      background: white;
    }
    .osuall_class_grade_wrapper:nth-child(2n){
      background: #eee;
    }
    .osuall_class_grade_wrapper{
      flex: 1;
      display: flex;
      border-radius: 0.5rem;
    }
    .osuall_class_grade_wrapper>div{
      flex: 1;
      padding: 0.5rem;
      word-break: break-all;
    }
    .osuall_assignment_wrapper:nth-child(2n+1){
      background: rgba(255,255,255,0.8);
    }
    .osuall_assignment_wrapper:nth-child(2n){
      background: rgba(255,255,255,0.4);
    }
    .osuall_assignment_wrapper{
      display: flex;
      padding: 0.5rem;
      border-radius: 0.5rem;
    }
    .osuall_assignment_wrapper>div{
      flex: 1;
      padding-right: 0.25rem;
      padding-left: 0.25rem;
    }
    .osuall_assignment_title{
      display: flex;
      align-items: center;
    }
    .osuall_assignment_title>img{
      height: 1.5rem;
      width: 1.5rem;
      margin-right: 0.5rem;
    }
    .osuall_status_submit,
    .osuall_status_submit a{
      color: green;
    }
    .osuall_status_done{
      text-decoration: line-through;
    }
    .osuall_status_late{
      background: orange !important;
      color: white;
    }
    .osuall_status_late a{
      color: white;
    }
    .osuall_status_missing{
      background: red !important;
      color: white;
    }
    .osuall_status_missing a{
      color: white;
    }
    .osuall_status_feedback>.osuall_assignment_title::after{
      content: " (Feedback available)"
    }
  </style>
  <span id="osuall_fetching_information">Fetching information... please wait...</span>
  <div id="osuall_main_wrapper">
    <div>
      <div id="osuall_class_wrapper">
        <!-- Classes, Grades, etc. -->
        <h3>Classes</h3>
        <div id="osuall_class_grades">
          <div id="osuall_class_grade_header" class="osuall_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="osuall_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="osuall_assignment_filter_list_chosen">
        </div>
      </div>
      <div id="osuall_assignments_wrapper">
        <div class="osuall_assignment_wrapper">
          <div>
            <span>Assignment Name</span>
          </div>
          <div>
            <span>Class</span>
          </div>
          <div>
            <span>Due Date</span>
          </div>
        </div>
      </div>
    </div>
    <div id="osuall_announcement_wrapper">
      <h3>Announcements</h3>
    </div>
  </div>`;
  iFrameLoader.id = "osu-all-iframe-loader";
  Main.append(iFrameLoader);

  /**
   * Classes - Class information
   * ClassAssignments - Assignments for classes
   * MainFrame - The main iframe
   * ENV - Global variables with useful data
   * current_user_id - The current user id
   */
  console.log("[OSU-ALL] - Getting Classes");
  const Classes = await GetClasses(),
    ClassAssignments = [],
    ClassGrades = {},
    {ENV} = window,
    {current_user_id} = ENV,
    ClassGradeDiv = Main.querySelector("#osuall_class_grades"),
    AssignmentsDiv = Main.querySelector("#osuall_assignments_wrapper"),
    AnnouncementsDiv = Main.querySelector("#osuall_announcement_wrapper"),
    StatusFilter = Main.querySelector("#osuall_assignment_filter_status"),
    StatusFilterList = Main.querySelector("#osuall_assignment_filter_list_chosen");

  // Filters
  StatusFilter.addEventListener("change",()=>{
    if(StatusFilter.value === ""){
      // ignore reset to empty value
      return;
    }
    const elem = StatusFilter.querySelector(`option[value="${StatusFilter.value}"]`),
      temp = document.createElement("template"),
      now = Date.now();
    temp.innerHTML = `<style id="osuall_filter_${now}">
      .osuall_status_${StatusFilter.value}{
        display: none;
      }
    </style>`;
    document.body.append(temp.content.cloneNode(true));
    StatusFilterList.append(elem);
    StatusFilter.value = "";
    function click(){
      // remove style
      StatusFilter.append(elem);
      document.querySelector(`#osuall_filter_${now}`).outerHTML = "";
      StatusFilter.value = "";
      elem.removeEventListener("click",click);
    }
    elem.addEventListener("click",click);
  });

  // Begin writing class information
  for(let i = 0;i<Classes.length;i++){
    const Class = Classes[i],
      template = document.createElement("template");
    template.innerHTML = `<div class="osuall_class_grade_wrapper" osuall-class-id="${Class.id}">
      <div class="osuall_class_grade_name">
        <span>${i + 1}. <a href="/courses/${Class.id}">${Class.name}</a></span>
      </div>
      <div class="osuall_class_grade_score">
        <span>(Loading scores...)</span>
      </div>
      <div class="osuall_class_grade_instructor">
        <span>(Loading instructors...)</span>
      </div>
    </div>`;
    ClassGradeDiv.append(template.content.cloneNode(true));
  }

  console.log("[OSU-ALL] - Getting Class Assignments and grades");
  for(const i in Classes){
    ClassAssignments.push.apply(ClassAssignments,await GetClassAssignments(Classes[i].id,current_user_id));
    // Get grades
    LoadFrame(iFrameLoader,`/courses/${Classes[i].id}/grades`).then((ClassGradeFrame)=>{
      const {document:d,window:w} = ClassGradeFrame;
      let Grades = d.querySelectorAll(".grade"),
        Titles = d.querySelectorAll(".title"),
        PossiblePoints = d.querySelectorAll(".points_possible"),
        DueDates = d.querySelectorAll(".due");
      ClassGrades[Classes[i].id] = {
        Grades,
        Titles,
        PossiblePoints,
        DueDates
      };
      // Write grades
      const GradeDiv = ClassGradeDiv.querySelector(`[osuall-class-id="${Classes[i].id}"] > .osuall_class_grade_score`),
        Grade = Array.from(Grades).reverse()[0].textContent,
        {ENV} = w,
        {grading_scheme} = ENV,
        [Letter] = grading_scheme.find((scheme)=>{
          if(isNaN(+Grade.textContent)){
            return 1 >= scheme[1];
          }
          return +Grade.textContent >= scheme[1];
        });
      GradeDiv.innerHTML = `<span>${Letter} (${Grade})</span>`;
      if(Object.keys(ClassGrades).length === Classes.length){
        // Done loading iframes all data, can remove any pointless memory now!
        iFrameLoader.innerHTML = "";
        for(const i in ClassGrades){
          delete ClassGrades[i];
        }
        Grades = Titles = PossiblePoints = DueDates = null;
      }
    });
    // Get instructors
    GetClassTeacher(Classes[i].id).then(async (Teacher)=>{
      const TeacherDiv = ClassGradeDiv.querySelector(`[osuall-class-id="${Classes[i].id}"] > .osuall_class_grade_instructor`);
      TeacherDiv.innerHTML = `<span>${Teacher.name}</span>`;
    }).catch(()=>{
      // no teacher, or failed to get teacher
      const TeacherDiv = ClassGradeDiv.querySelector(`[osuall-class-id="${Classes[i].id}"] > .osuall_class_grade_instructor`);
      TeacherDiv.innerHTML = "<span>No instructor found.</span>";
    });
  }
  // Write assignments
  ClassAssignments.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();
  });
  for(let j = 0;j<ClassAssignments.length;j++){
    const Assignment = ClassAssignments[j],
      template = document.createElement("template");
    if(Assignment.plannable_type === "announcement"){
      // Put in announcement thing
      template.innerHTML = `<div class="osuall_announcement_wrapper">
          <div class="osuall_announcement_title">
            <a href="${Assignment.html_url}">${Assignment.plannable.title}</a>
          </div>
          <div class="osuall_announcement_class">
            <a href="/courses/${Assignment.course_id}">${Assignment.context_name}</a>
          </div>
          <div class="osuall_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;
    }
    let icon;
    switch (Assignment.plannable_type){
      case "assignment":{
        icon = "https://cdn.discordapp.com/attachments/775828441127714837/796079921109270598/assignment.svg";
        break;
      }
      case "quiz":{
        icon = "https://cdn.discordapp.com/attachments/775828441127714837/796171033434521671/quiz.svg";
        // old icon - "https://cdn.discordapp.com/attachments/775828441127714837/796080854190260274/quiz.svg";
        break;
      }
    }
    const classes = [],
      {submissions} = Assignment;
    if(submissions){
      const {excused,graded,has_feedback,late,missing,submitted} = submissions;
      if(excused || graded){
        classes.push("osuall_status_done");
      }
      if(submitted){
        classes.push("osuall_status_submit");
      }
      if(late){
        classes.push("osuall_status_late");
      }
      if(missing){
        classes.push("osuall_status_missing");
      }
      if(has_feedback){
        classes.push("osuall_status_feedback");
      }
    }
    classes.push("osuall_status_" + Assignment.plannable_type);
    template.innerHTML = `<div class="osuall_assignment_wrapper ${classes.join(" ")}" class-id="${Assignment.course_id}">
      <div class="osuall_assignment_title osuall_assignment_type_${icon}">
        <img src="${icon}" alt="${Assignment.plannable_type}" class="osuall_assignment_icon">
        <a href="${Assignment.html_url}">${Assignment.plannable.title}</a>
      </div>
      <div class="osuall_assignment_class">
        <a href="/courses/${Assignment.course_id}">${Assignment.context_name}</a>
      </div>
      <div class="osuall_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));
  }
  ClassAssignments.splice(0); // Free up memory
  document.querySelector("#osuall_fetching_information").outerHTML = "";
}
// /api/v1/users/:user_id/communication_channels
/**
 * GetClasses - Gets all the classes of the user
 *
 * @returns {Promise<Array>} A list of class information
 */
function GetClasses(){
  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.send();
    x.onload = ()=>{
      res(JSON.parse(x.responseText));
    };
    x.onerror = ()=>{
      rej();
    };
  });
}

/**
 * GetClassAssignments - Gets the class assignments
 *
 * @param  {String} ClassID The class id
 * @param  {String} UserID The user id
 * @returns {Promise<Array>} The list of assignments
 */
function GetClassAssignments(ClassID,UserID){
  return new Promise((res,rej)=>{
    const x = new XMLHttpRequest(),
      now = new Date(),
      offset = now.getTimezoneOffset() / 60;
    now.addDays(-14);
    now.setHours(8 - offset);
    now.setMinutes(0);
    now.setSeconds(0);
    now.setMilliseconds(0);
    x.open("GET",`/api/v1/planner/items?start_date=${now.toISOString()}&order=asc&context_codes[]=course_${ClassID}&context_codes[]=user_${UserID}`);
    x.setRequestHeader("X-Requested-With","XMLHttpRequest");
    x.setRequestHeader("accept","application/json+canvas-string-ids, application/json+canvas-string-ids, application/json, text/plain, */*");
    x.send();
    x.onerror = ()=>{
      rej();
    };
    x.onload = ()=>{
      res(JSON.parse(x.responseText));
    };
  });
}

/**
 * GetClassTeacher - Gets the teacher of the class
 *
 * @param  {String} ClassID The class id
 * @returns {Object} The teacher
 */
function GetClassTeacher(ClassID){
  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_${ClassID}_teachers`);
  x.setRequestHeader("X-Requested-With","XMLHttpRequest");
  x.setRequestHeader("Accept","application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01");
  x.send();
  return new Promise((res,rej)=>{
    x.onload = ()=>{
      res(JSON.parse(x.responseText)[0]);
    };
    x.onerror = ()=>{
      rej();
    };
  });
}

/**
 * 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((Resolve)=>{
    const MainFrame = document.createElement("iframe");
    MainFrame.src = src || "/";
    iFrameLoader.append(MainFrame);
    MainFrame.addEventListener("load",()=>{
      const Document = {document:MainFrame.contentDocument,window:MainFrame.contentWindow};
      Resolve(Document);
    });
  });
}

Load();