|
// ==UserScript== |
|
// @name Polyratings (Student Center) |
|
// @namespace http://github.com/Chris2fourlaw |
|
// @version 0.1.4 |
|
// @description List ratings for instructors in Student Center |
|
// @author Chris Lawson |
|
// @match *://cmsweb.pscs.calpoly.edu/* |
|
// @icon https://polyratings.org/favicon.ico |
|
// @downloadURL https://gist.github.com/Chris2fourlaw/fc93524caab046b35d99e1e0be3a3b0a/raw/polyratings_sc.user.js |
|
// @updateURL https://gist.github.com/Chris2fourlaw/fc93524caab046b35d99e1e0be3a3b0a/raw/polyratings_sc.user.js |
|
// @grant GM_getValue |
|
// @grant GM_setValue |
|
// @grant GM_addStyle |
|
// @grant GM_registerMenuCommand |
|
// @require https://code.jquery.com/jquery-3.6.0.min.js |
|
// @require https://raw.github.com/odyniec/MonkeyConfig/master/monkeyconfig.js |
|
// ==/UserScript== |
|
|
|
// Menu Config |
|
const cfg = new MonkeyConfig({ |
|
title: "Polyratings (Student Center) Configuration", |
|
menuCommand: true, |
|
params: { |
|
colors: { |
|
type: "checkbox", |
|
default: true, |
|
}, |
|
}, |
|
}); |
|
|
|
// DOM observer |
|
const observeDOM = (() => { |
|
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver; |
|
|
|
return (obj, callback) => { |
|
if (!obj || obj.nodeType !== 1) return; |
|
|
|
if (MutationObserver) { |
|
// define a new observer |
|
const mutationObserver = new MutationObserver(callback); |
|
|
|
// have the observer observe foo for changes in children |
|
mutationObserver.observe(obj, { childList: true, subtree: true }); |
|
return mutationObserver; |
|
} |
|
|
|
// browser support fallback |
|
if (window.addEventListener) { |
|
obj.addEventListener('DOMNodeInserted', callback, false); |
|
obj.addEventListener('DOMNodeRemoved', callback, false); |
|
} |
|
}; |
|
})(); |
|
|
|
// Get all professors from API |
|
function getAllProfs() { |
|
return fetch('https://api-prod.polyratings.org/professors') |
|
.then((res) => res.json()) |
|
.then((data) => data); |
|
} |
|
|
|
// Find intersecting |
|
function intersectingDbEntities(arrays) { |
|
if (arrays.length === 1) { |
|
return { |
|
intersect: arrays[0], |
|
nonIntersect: [], |
|
}; |
|
} |
|
const idToEntity = arrays.flat().reduce((acc, curr) => { |
|
acc[curr.id] = curr; |
|
return acc; |
|
}, {}); |
|
const idArrays = arrays.map((arr) => arr.map((x) => x.id)); |
|
let intersectionSet = new Set(idArrays[0]); |
|
idArrays.slice(1).forEach((array) => { |
|
const compareSet = new Set(array); |
|
intersectionSet = new Set([...intersectionSet].filter((x) => compareSet.has(x))); |
|
}); |
|
|
|
const nonIntersect = arrays.flat().filter((x) => !intersectionSet.has(x.id)); |
|
|
|
return { |
|
intersect: Array.from(intersectionSet).map((id) => idToEntity[id]), |
|
nonIntersect, |
|
}; |
|
} |
|
|
|
// Search API response for professor |
|
function searchForProf(profName) { |
|
const tokens = profName.toLowerCase().split(' '); |
|
|
|
let tokenMatches; |
|
|
|
// If style is F. Last |
|
if (tokens[0][1] === '.') { |
|
tokenMatches = tokens.map((token) => |
|
profList.filter((prof) => |
|
`${prof.firstName[0]}. ${prof.lastName}`.toLowerCase().includes(token), |
|
), |
|
); |
|
} else { |
|
tokenMatches = tokens.map((token) => |
|
profList.filter((prof) => |
|
`${prof.lastName}, ${prof.firstName}`.toLowerCase().includes(token), |
|
), |
|
); |
|
} |
|
|
|
const { intersect } = intersectingDbEntities(tokenMatches); |
|
return intersect[0]; |
|
} |
|
|
|
// Get professor rating from API |
|
function getProfRating(profName) { |
|
const rating = searchForProf(profName)?.overallRating; |
|
|
|
if (rating) { |
|
return rating.toLocaleString('en', { useGrouping: false, minimumFractionDigits: 1 }); |
|
} |
|
|
|
return ""; |
|
} |
|
|
|
// Get row color |
|
function getRowColor(rating) { |
|
if (rating > 3.0) { |
|
return '#D9EAD3'; |
|
} |
|
if (rating > 2.5) { |
|
return '#FFF2CC'; |
|
} |
|
if (rating > 1.75) { |
|
return '#FDE5CD'; |
|
} |
|
return '#F4CCCC'; |
|
} |
|
|
|
// Update the tables |
|
function modifyTables() { |
|
// Schedule table & Shopping cart |
|
$( |
|
'table[id^="STDNT_ENRL_"], table[id^="SSR_REGFORM_"], table[id^="CLASS_MTG_"], table[id^="SSR_CLSRCH_"]', |
|
).each((i, table) => { |
|
let innerTable; |
|
let instructorCol; |
|
|
|
innerTable = $(table).find('table')[0]; |
|
innerTable = innerTable ? innerTable : table // If there is no inner table, try the outer one |
|
|
|
// Create header |
|
const headers = $(innerTable).find('th'); |
|
const polyRatingHeader = $(innerTable).find('th[abbr="PolyRating"]'); |
|
headers.each((j, th) => { |
|
if (th.abbr === 'Instructor' && polyRatingHeader.length === 0) { |
|
instructorCol = j; |
|
const alignment = th.align ? th.align : 'left'; |
|
$(th).after( |
|
`<th scope="col" abbr="PolyRating" width="80" align="${alignment}" class="PSLEVEL1GRIDCOLUMNHDR"> |
|
<a tabindex="291" class="PSLEVEL1GRIDCOLUMNHDR" href="https://polyratings.org" target="_blank" rel="noopener noreferrer"> |
|
Polyrating |
|
</a> |
|
</th>`, |
|
); |
|
} |
|
}); |
|
|
|
// Populate ratings data |
|
$(innerTable) |
|
.find('tr') |
|
.each((j, tr) => { |
|
const cells = $(tr).find('td'); |
|
const instructorCell = cells[instructorCol]; |
|
const polyRatingCol = $(tr).find('td[abbr="PolyRatingCell"]'); |
|
|
|
if (instructorCell && polyRatingCol.length === 0) { |
|
const prof = searchForProf(instructorCell.innerText); |
|
const ratingText = prof ? `${prof?.overallRating} / 4.0 (${prof?.numEvals})` : '<a href="https://polyratings.org/new-teacher" target="_blank" rel="noopener noreferrer">Add</a>'; |
|
$(instructorCell).after( |
|
`<td class="PSLEVEL1GRIDODDROW" abbr="PolyRatingCell"> |
|
<a href="https://polyratings.org/professor/${prof?.id}" target="_blank" rel="noopener noreferrer"> |
|
${ratingText} |
|
</a> |
|
</td>`, |
|
); |
|
|
|
// Add row colors if enabled |
|
if (prof && cfg.get('colors')) { |
|
$(tr) |
|
.find('td') |
|
.each((k, cell) => |
|
$(cell).css('background-color', getRowColor(prof?.overallRating)), |
|
); |
|
|
|
// These attributes change the bg color |
|
$(tr).removeAttr('onclick'); |
|
$(tr).removeAttr('onmouseout'); |
|
$(tr).removeAttr('onmouseover'); |
|
} |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
// Globalize the full professor list |
|
const profList = await getAllProfs(); |
|
|
|
// Main function |
|
$(document).ready(() => { |
|
modifyTables(); |
|
}); |
|
|
|
document.body.childNodes.forEach((node) => { |
|
observeDOM(node, () => { |
|
modifyTables(); |
|
}); |
|
}); |
This is clean. Nice job!