Terp Course Helper

Integrate Rate My Professor to Testudo Schedule of Classes

目前为 2019-04-13 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Terp Course Helper
  3. // @author DickyT
  4. // @license WTFPL
  5. // @encoding utf-8
  6. // @date 04/12/2019
  7. // @modified 04/12/2019
  8. // @include https://app.testudo.umd.edu/soc/*
  9. // @grant GM_xmlhttpRequest
  10. // @run-at document-end
  11. // @version 0.0.6
  12. // @description Integrate Rate My Professor to Testudo Schedule of Classes
  13. // @namespace dkt.umdrmp.testudo
  14. // @require https://unpkg.com/ajax-hook/dist/ajaxhook.min.js
  15. // ==/UserScript==
  16.  
  17. const DATA = {
  18. rmp: {},
  19. pt: {},
  20. };
  21. let ALIAS = {};
  22.  
  23. function loadAliasTable() {
  24. return new Promise((resolve) => {
  25. const url = 'https://raw.githubusercontent.com/DickyT/Terp-Course-Helper/master/alias.json';
  26. GM_xmlhttpRequest({
  27. method: 'GET',
  28. url,
  29. onload: (data) => {
  30. if (data.status == 200) {
  31. ALIAS = JSON.parse(data.responseText);
  32. }
  33. resolve();
  34. }
  35. });
  36. });
  37. }
  38.  
  39. function getInstructorName(elem) {
  40. const container = elem.childNodes[0];
  41. if (container instanceof HTMLAnchorElement) {
  42. return container.innerText;
  43. }
  44. return container.wholeText;
  45. }
  46.  
  47. function updateInstructorRating() {
  48. // unsafeWindow.console.log(DATA.rmp);
  49. const instructorElements = unsafeWindow.document.querySelectorAll('.section-instructor');
  50. Array.prototype.map.call(instructorElements, (elem) => {
  51. const instructorName = getInstructorName(elem);
  52. if (DATA.rmp[instructorName]) {
  53. const oldElem = elem.querySelector('.rmp-rating-box');
  54. if (oldElem) {
  55. oldElem.remove();
  56. }
  57. const rating = DATA.rmp[instructorName].rating;
  58. const ratingElem = document.createElement('a');
  59. ratingElem.className = 'rmp-rating-box';
  60. ratingElem.href = rating ? `https://www.ratemyprofessors.com/ShowRatings.jsp?tid=${DATA.rmp[instructorName].recordId}` : '';
  61. ratingElem.title = instructorName;
  62. ratingElem.target = '_blank';
  63. ratingElem.innerText = rating ? rating.toFixed(1) : 'N/A';
  64. elem.appendChild(ratingElem);
  65. }
  66. });
  67.  
  68. updatePTData();
  69. }
  70.  
  71. function getRecordId(name) {
  72. return new Promise((resolve, reject) => {
  73. // unsafeWindow.console.log(ALIAS, name);
  74. if (ALIAS[name]) {
  75. const recordId = ALIAS[name].rmpId;
  76. if (recordId) {
  77. return resolve(recordId);
  78. }
  79. }
  80. const url = `https://search-production.ratemyprofessors.com/solr/rmp/select?q=${encodeURIComponent(name)}&defType=edismax&qf=teacherfullname_t%5E1000%20autosuggest&bf=pow%28total_number_of_ratings_i%2C2.1%29&siteName=rmp&rows=20&start=0&fl=pk_id%20teacherfirstname_t%20teacherlastname_t%20total_number_of_ratings_i%20schoolname_s%20averageratingscore_rf%20averageclarityscore_rf%20averagehelpfulscore_rf%20averageeasyscore_rf%20chili_i%20schoolid_s%20teacherdepartment_s&fq=schoolid_s%3A1270&wt=json`;
  81. GM_xmlhttpRequest({
  82. method: 'GET',
  83. url,
  84. onload: (data) => {
  85. if (data.status == 200) {
  86. const res = JSON.parse(data.responseText);
  87.  
  88. const suggestionList = res.response.docs;
  89. const [instructorInfo] = suggestionList.filter(d => d.schoolid_s === '1270');
  90. if (instructorInfo) {
  91. // unsafeWindow.console.log(instructorInfo);
  92. return resolve(instructorInfo.pk_id);
  93. }
  94. }
  95. reject();
  96. }
  97. });
  98. });
  99. }
  100.  
  101. function getRating(recordId) {
  102. return new Promise((resolve, reject) => {
  103. const url = `https://www.ratemyprofessors.com/ShowRatings.jsp?tid=${recordId}`;
  104. GM_xmlhttpRequest({
  105. method: 'GET',
  106. url,
  107. onload: (data) => {
  108. if (data.status == 200) {
  109. const res = data.responseText;
  110. const reader = document.implementation.createHTMLDocument('reader'); // prevent loading any resources
  111. const fakeHtml = reader.createElement('html');
  112. fakeHtml.innerHTML = res;
  113. const ratingRawElem = fakeHtml.querySelector('#mainContent div.grade');
  114. if (ratingRawElem) {
  115. return resolve(Number(ratingRawElem.innerText));
  116. }
  117. }
  118. reject();
  119. }
  120. });
  121. });
  122. }
  123.  
  124. function loadRateData() {
  125. const instructorElements = unsafeWindow.document.querySelectorAll('.section-instructor');
  126. Array.prototype.map.call(instructorElements, (elem) => {
  127. const instructorName = getInstructorName(elem);
  128. if (!DATA.rmp[instructorName]) {
  129. DATA.rmp[instructorName] = {
  130. name: instructorName,
  131. };
  132. getRecordId(instructorName).then((recordId) => {
  133. getRating(recordId).then((rating) => {
  134. DATA.rmp[instructorName].recordId = recordId;
  135. DATA.rmp[instructorName].rating = rating;
  136.  
  137. updateInstructorRating();
  138. }).catch(() => {
  139. updateInstructorRating();
  140. });
  141. }).catch(() => {
  142. updateInstructorRating();
  143. });
  144. }
  145. });
  146. }
  147.  
  148. function getPTCourseData(courseId) {
  149. return new Promise((resolve, reject) => {
  150. const url = `https://planetterp.com/course/${courseId}`;
  151. GM_xmlhttpRequest({
  152. method: 'GET',
  153. url,
  154. onload: (data) => {
  155. if (data.status == 200) {
  156. const res = data.responseText;
  157. const reader = document.implementation.createHTMLDocument('reader'); // prevent loading any resources
  158. const fakeHtml = reader.createElement('html');
  159. fakeHtml.innerHTML = res;
  160.  
  161. const courseData = {
  162. courseId,
  163. instructors: {},
  164. };
  165. const avgGPAElem = fakeHtml.querySelector('#course-grades > p.center-text');
  166. if (avgGPAElem) {
  167. const matchRes = avgGPAElem.innerText.match(/Average GPA: ([0-9]\.[0-9]{2})/);
  168. if (matchRes && matchRes[1]) {
  169. const avgGPA = Number(matchRes[1]);
  170. if (!Number.isNaN(avgGPA)) {
  171. courseData.avgGPA = avgGPA;
  172. }
  173. }
  174. }
  175.  
  176. const instructorReviewElementList = fakeHtml.querySelectorAll('#course-professors > div');
  177. Array.prototype.map.call(instructorReviewElementList, (instructorCardElem) => {
  178. const instructorNameElem = instructorCardElem.querySelector('.card-header a');
  179. if (instructorNameElem) {
  180. const instructorName = instructorNameElem.innerText;
  181. const instructorId = instructorNameElem.getAttribute('href').replace(/^\/professor\//, '');
  182.  
  183. const reviewElement = instructorCardElem.querySelector('.card-text');
  184. if (reviewElement) {
  185. const res = reviewElement.innerText.match(/Average rating: ([0-9]\.[0-9]{2})/);
  186. if (res && res[1]) {
  187. const rating = Number(res[1]);
  188. if (!Number.isNaN(rating)) {
  189. courseData.instructors[instructorName] = {
  190. name: instructorName,
  191. id: instructorId,
  192. rating,
  193. }
  194. }
  195. }
  196. }
  197. }
  198. });
  199.  
  200. return resolve(courseData);
  201. }
  202. reject();
  203. }
  204. });
  205. });
  206. }
  207.  
  208. function updatePTData() {
  209. const allCourseElem = document.querySelectorAll('#courses-page .course');
  210. Array.prototype.map.call(allCourseElem, (courseElem) => {
  211. const courseIdElem = courseElem.querySelector('.course-id');
  212. const courseId = courseIdElem.innerText;
  213. const courseIdContainer = courseIdElem.parentNode;
  214.  
  215. const oldElem = courseElem.querySelector('.pt-gpa-box');
  216. if (oldElem) {
  217. oldElem.remove();
  218. }
  219.  
  220. if (DATA.pt[courseId]) {
  221. const avgGPA = DATA.pt[courseId].avgGPA;
  222.  
  223. const avgGPAElem = document.createElement('a');
  224. avgGPAElem.className = 'pt-gpa-box';
  225. avgGPAElem.href = `https://planetterp.com/course/${courseId}`;
  226. avgGPAElem.title = courseId;
  227. avgGPAElem.target = '_blank';
  228. avgGPAElem.innerText = avgGPA ? `AVG GPA ${avgGPA.toFixed(2)}` : 'N/A';
  229. courseIdContainer.appendChild(avgGPAElem);
  230. }
  231.  
  232. const instructorElemList = courseElem.querySelectorAll('.section-instructor');
  233. Array.prototype.map.call(instructorElemList, (elem) => {
  234. const instructorName = getInstructorName(elem);
  235. if (DATA.pt[courseId] && DATA.pt[courseId].instructors[instructorName]) {
  236. const oldElem = elem.querySelector('.pt-rating-box');
  237. if (oldElem) {
  238. oldElem.remove();
  239. }
  240.  
  241. const rating = DATA.pt[courseId].instructors[instructorName].rating;
  242. const ratingElem = document.createElement('a');
  243. ratingElem.className = 'pt-rating-box';
  244. ratingElem.href = rating ? `https://planetterp.com/professor/${DATA.pt[courseId].instructors[instructorName].id}` : '';
  245. ratingElem.title = instructorName;
  246. ratingElem.target = '_blank';
  247. ratingElem.innerText = rating ? rating.toFixed(2) : 'N/A';
  248. elem.appendChild(ratingElem);
  249. }
  250. });
  251. });
  252. }
  253.  
  254. function loadPTData() {
  255. const courseIdElements = document.querySelectorAll('.course-id');
  256.  
  257. let count = 0;
  258.  
  259. function tryUpdateUI() {
  260. count += 1;
  261.  
  262. if (count >= courseIdElements.length) {
  263. updatePTData();
  264. }
  265. }
  266.  
  267. Array.prototype.map.call(courseIdElements, (elem) => {
  268. const courseId = elem.innerText;
  269. if (!DATA.pt[courseId]) {
  270. DATA.pt[courseId] = {
  271. courseId,
  272. };
  273. getPTCourseData(courseId).then((courseData) => {
  274. DATA.pt[courseId] = courseData;
  275. tryUpdateUI();
  276. }).catch(() => {
  277. tryUpdateUI();
  278. });
  279. }
  280. });
  281. }
  282.  
  283. unsafeWindow.window.x = updatePTData;
  284.  
  285. function main() {
  286. loadAliasTable().then(() => {
  287. // First load
  288. loadPTData();
  289. loadRateData();
  290. // Add hook to HTTP events
  291. const hookAjax = unsafeWindow.window.hookAjax;
  292. hookAjax({
  293. onreadystatechange: (xhr) => {
  294. if (/https?:\/\/app.testudo.umd.edu\/soc\/[0-9]{6}\/sections\?*/.test(xhr.responseURL)) {
  295. if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
  296. setTimeout(loadRateData, 200);
  297. }
  298. }
  299. },
  300. });
  301. });
  302. }
  303.  
  304. const ajaxHookLib = document.createElement('script');
  305. ajaxHookLib.addEventListener('load', main);
  306. ajaxHookLib.src = 'https://unpkg.com/ajax-hook/dist/ajaxhook.min.js';
  307. document.head.appendChild(ajaxHookLib);
  308.  
  309. const styleInject = `
  310. .rmp-rating-box,
  311. .pt-rating-box {
  312. border-radius: 5px;
  313. padding: 1px 5px;
  314. margin-left: 10px;
  315. background-color: #FF0266;
  316. color: #FFFFFF !important;
  317. font-family: monospace;
  318. font-weight: bold;
  319. }
  320.  
  321. .pt-rating-box {
  322. background-color: #009688;
  323. }
  324.  
  325. .pt-gpa-box {
  326. display: flex;
  327. text-align: center;
  328. margin-top: 10px;
  329. border-radius: 5px;
  330. background-color: #009688;
  331. color: #FFFFFF !important;
  332. font-family: monospace;
  333. font-weight: bold;
  334. padding: 1px;
  335. }
  336. `;
  337. const styleInjectElem = document.createElement('style');
  338. styleInjectElem.id = 'umd-rmp-style-inject';
  339. styleInjectElem.innerHTML = styleInject;
  340. document.head.appendChild(styleInjectElem);