Canvas User Joined Date

Fetch each user's profile page on Canvas roster, extract the "Created" date, and add a new "Joined On" column reliably.

От 22.10.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Canvas User Joined Date
// @namespace    hacker09
// @version      1
// @description  Fetch each user's profile page on Canvas roster, extract the "Created" date, and add a new "Joined On" column reliably.
// @match        https://canvas.instructure.com/courses/*/users
// @icon         https://du11hjcvx0uqb.cloudfront.net/br/dist/images/favicon-e10d657a73.ico
// @run-at       document-end
// ==/UserScript==

(function() {
  'use strict'

  let enrollmentData = [] //global array for debugging all td innerHTML
  let pending = 0 //counter for pending fetch requests

  function extractCreatedDate(tdInnerHTML) { //helper: extract Created date from td innerHTML
    let text = tdInnerHTML.replace(/<br\s*\/?>/gi, ' ') //replace <br> with space
    .replace(/<\/?[^>]+(>|$)/g, '') //remove all other HTML tags
    .replace(/\s+/g, ' ') //normalize multiple spaces to single space
    .trim(); //remove leading/trailing whitespace
    let match = text.match(/Created\s+(.+?)(?:\s+Completed|$)/i) //match "Created" followed by date until "Completed" or end
    if(!match) { //if first pattern fails
      match = text.match(/Created\s+(.+)/i) //fallback: grab everything after "Created"
    }
    return match ? match[1].trim() : 'N/A'; //return extracted date or N/A if not found
  }

  function processTable(){ //main function to process the roster table
    let header=document.querySelector("table.roster thead tr") //find table header row
    if(header && !document.querySelector("th.joined-on")) { //if header exists and joined column not added yet
      let th=document.createElement("th") //create new header cell
      th.className="joined-on" //add class for identification
      th.textContent="Joined On" //set header text
      th.scope="col" //add accessibility attribute
      let lastTh = header.querySelector('th:last-child') //find last header (admin links)
      header.insertBefore(th, lastTh) //insert before admin links column
    }

    let rows=document.querySelectorAll("tbody.collectionViewItems tr:not(.joined-processed)") //find unprocessed rows
    rows.forEach(row=>{ //iterate through each row
      row.classList.add("joined-processed") //mark row as processed
      let link=row.querySelector('a[href^="https://canvas.instructure.com/courses/"][href*="/users/"]') //find user profile link
      if(!link) return //skip if no link found

      let td=document.createElement("td") //create new data cell
      td.textContent="Loading..." //set loading text
      let lastTd = row.querySelector('td:last-child') //find last cell (admin links)
      row.insertBefore(td, lastTd) //insert before admin links cell

      pending++ //increment pending counter
      fetch(link.href).then(r=>r.text()).then(html=>{ //fetch user profile page
        let parser=new DOMParser() //create HTML parser
        let doc=parser.parseFromString(html,"text/html") //parse fetched HTML
        let cell=doc.querySelector('td[style*="width: 50%"]') //find enrollment info cell
        if(cell){ //if cell found
          enrollmentData.push(cell.innerHTML.trim()) //store full HTML for debugging
          td.textContent = extractCreatedDate(cell.innerHTML) //extract and display date
        } else { //if cell not found
          enrollmentData.push("NO TD FOUND") //log error for debugging
          td.textContent = "N/A" //display N/A
        }
      }).catch(()=>{ //handle fetch errors
        enrollmentData.push("FETCH ERROR") //log error for debugging
        td.textContent = "Error" //display error
      }).finally(()=>{ //always execute after fetch
        pending-- //decrement pending counter
        if(pending===0){ //if all requests completed
          console.log("All fetched td innerHTML:", enrollmentData) //log all data for debugging
        }
      })
    })
  }

  window.addEventListener('load', ()=>{ //wait for window load event
    setTimeout(processTable, 500) //delay to ensure dynamic content loaded
  })

  let observer=new MutationObserver(()=>{ //create observer for DOM changes
    clearTimeout(window.canvasJoinedDateTimeout) //clear previous timeout
    window.canvasJoinedDateTimeout = setTimeout(processTable, 100) //debounce with 100ms delay
  })

  if(document.readyState === 'loading') { //if document still loading
    document.addEventListener('DOMContentLoaded', ()=>{ //wait for DOM ready
      observer.observe(document.body,{childList:true,subtree:true}) //start observing body for changes
    })
  } else { //if document already loaded
    observer.observe(document.body,{childList:true,subtree:true}) //start observing immediately
  }
})()