NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Tufts Course Scheduler Auto-Sign-Up // @namespace 71c // @version 0.4.4 // @description To be used with tuftscoursescheduler.com; automatically signs up for classes at Tufts // @homepageURL https://github.com/71c/course_scheduler // @author 71c // @copyright 2020, 71c (https://openuserjs.org/users/71c) // @updateURL https://openuserjs.org/meta/71c/Tufts_Course_Scheduler_Auto-Sign-Up.meta.js // @downloadURL https://openuserjs.org/install/71c/Tufts_Course_Scheduler_Auto-Sign-Up.user.js // @license MIT // @match https://sis.it.tufts.edu/psp/paprd/EMPLOYEE/EMPL/h/* // @match http://localhost:5000/schedule* // @match https://tuftscoursescheduler.com/schedule* // @match https://tuftscoursescheduler.com/schedule* // @match https://sis.it.tufts.edu/psp/paprd/EMPLOYEE/PSFT_SA/s/WEBLIB_CLS_SRCH.ISCRIPT1.FieldFormula.IScript_GoToCart // @match https://siscs.it.tufts.edu/psc/csprd/EMPLOYEE/PSFT_SA/c/SA_LEARNER_SERVICES_2.SSR_SSENRL_CART.GBL???Page=SSR_SSENRL_CART&Action=A&INSTITUTION=TUFTS&TargetFrameName=Tfp_cart_iframe* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // ==/UserScript== 'use strict'; // Used to be "uit.tufts.edu" // Now it's "it.tufts.edu" const MAIN_DOMAIN = 'it.tufts.edu'; // http://mths.be/unsafewindow window.unsafeWindow || ( unsafeWindow = (function() { var el = document.createElement('p'); el.setAttribute('onclick', 'return window;'); return el.onclick(); }()) ); var jQuery; if (window.location.origin === `https://sis.${MAIN_DOMAIN}` || window.location.origin === `https://siscs.${MAIN_DOMAIN}`) { // we're at one of the SIS sites document.addEventListener('DOMContentLoaded', function() { jQuery = unsafeWindow.jQuery; whenOnSIS(); }); } else { // we're at my website unsafeWindow.hasUserscript = true; document.addEventListener('startUserscript', whenOnMyWebsite); } const getEnrollmentCartURL = `https://sis.${MAIN_DOMAIN}/psp/paprd/EMPLOYEE/PSFT_SA/s/WEBLIB_CLS_SRCH.ISCRIPT1.FieldFormula.IScript_GoToCart`; const baseURL = `https://sis.${MAIN_DOMAIN}/psp/paprd/EMPLOYEE/EMPL/h/`; const searchSearch = "?tab=TFP_CLASS_SEARCH"; var whenOnSIS = function() { unsafeWindow.addClassesToCart = function() { const info = JSON.parse(GM_getValue('classes', '{}')); console.log(window.location.href); whenOnSIS.addClasses(info); }; const homeSearch = "?tab=DEFAULT"; if (window.location.search.indexOf(homeSearch) === 0) { // we're at SIS home // whenOnSIS.makeAutoSignUpButton(); } else if (window.location.search.indexOf(searchSearch) === 0) { // we're at one of the search pages if (GM_getValue('setClassesImmediately', false)) { // we want to do it immediately if (!GM_getValue('clearClasses', false)) { // next time we go to url don't auto; only do once // turn setClassesImmediately off only if clearClasses is false because otherwise, classes won't be deleted from cart when clearClasses is true GM_setValue('setClassesImmediately', false); unsafeWindow.addClassesToCart(); } } else { // we don't want to do it immediately // whenOnSIS.addManualEntryUI(); } } else if (window.location.href === getEnrollmentCartURL) { if (GM_getValue('setClassesImmediately', false)) { // we're at a page that gives us a URL; immediately redirect to that URL const url = document.querySelector('span#IS_AC_RESPONSE').innerText.trim(); location.href = url; } } else if (window.location.href.indexOf(`https://siscs.${MAIN_DOMAIN}/psc/csprd/EMPLOYEE/PSFT_SA/c/SA_LEARNER_SERVICES_2.SSR_SSEN`) === 0) { // we're in the iframe inside the Enrollment Cart page whenOnSIS.deleteClassesFromCart(); } } whenOnSIS.waitFor = function(condition, callback) { /* wait for condition() to evaluate to to true, then execute callback */ if (condition()) callback(); else { var t = function() { setTimeout(function() { if (condition()) callback(); else t(); }, 100); }; t(); } } whenOnSIS.executeSequentially = function(functions, callback) { /* Execute asynchronous functions one-by-one. functions is an array of functions which each take a callback as a parameter. callback is executed when all the functions have finished executing. Yes, it would make more sense to use promises but this works fine */ if (functions.length === 0) callback(); else functions[0](function() { whenOnSIS.executeSequentially(functions.slice(1), callback); }); } whenOnSIS.addClasses = function(info) { const functions = info.classes.map(classInfo => whenOnSIS.addClass(info.term_code, info.career, /^[A-Z]+/.exec(classInfo.course_num)[0], /-.*/.exec(classInfo.course_num)[0].slice(1), classInfo.classNums, classInfo.title)); whenOnSIS.executeSequentially(functions, function() { console.log('done'); }); } whenOnSIS.addClass = function(term_code, career, subject, num, classNums, title) { return function(callback) { if (window.location.search.indexOf("?tab=TFP_CLASS_SEARCH") !== 0) { return; } window.location.hash = "#search_results/term/" + term_code + "/career/" + career + "/subject/" + subject + "/course/" + num + "/attr/keyword/" + title + "/instructor"; whenOnSIS.waitFor(function() { return !jQuery('.tfp-results-overlay')[0] && !jQuery('.tfp_cls_srch_loading')[0] && jQuery('.accorion-head')[0] && jQuery('td:contains(' + classNums[0] + ')')[0]; }, function() { if (document.querySelector('.tfp-offstate') !== null) { alert("You can't add to this term now"); return; } // click the checkbox to show sections. // if there is more than one result, sections will be hidden, and clicking the checkbox will show the sections // if there is one result, clicking the checkbox won't do anything jQuery('.tfp-show-result-sect').click(); whenOnSIS.selectSections(classNums, subject, num, callback); }); }; } whenOnSIS.selectSections = function(classNums, subject, num, callback) { for (const classNum of classNums) { const inputBubbleOrSpan = jQuery('td:contains(' + classNum + ')')[0].parentElement.children[6].children[0]; if (inputBubbleOrSpan.nodeName === "SPAN") { // the section is in cart or enrolled if (inputBubbleOrSpan.innerHTML === "In Cart") { alert("Section with class num " + classNum + " in course " + subject + "-" + num + " is already in your cart. Continuing."); } else if (inputBubbleOrSpan.innerHTML === "Enrolled") { alert("You have already enrolled for section with class num " + classNum + " in course " + subject + "-" + num + ". Continuing."); } else { // this shouldn't happen console.error("something unexpected happened"); return; } callback(); } else if (inputBubbleOrSpan.nodeName === "INPUT") { // just making sure if (!inputBubbleOrSpan.disabled) { inputBubbleOrSpan.click(); } else { alert("You can't add classes now"); return; } } else { // this shouldn't happen console.error("something unexpected happened"); return; } } jQuery('button:contains(Add to Cart)').click(); callback(); } whenOnSIS.makeAutoSignUpButton = function() { const button = document.createElement('button'); button.innerHTML = 'auto sign up'; button.onclick = function() { window.location.href = baseURL + searchSearch; }; document.body.appendChild(button); } whenOnSIS.addManualEntryUI = function() { const div = document.createElement('div'); const textarea = document.createElement('textarea'); const button = document.createElement('button'); button.innerHTML = 'add classes'; button.onclick = function() { // extensive error checking try { var info = JSON.parse(textarea.value); } catch (e) { alert('invalid JSON'); return; } for (const key of ["term_code", "career", "classes"]) { if (!(key in info)) { alert("missing attribute " + key); return; } } for (var i = 0; i < info.classes.length; i++) { const classInfo = info.classes[i]; for (const key of ["course_num", "classNums"]) { if (!(key in classInfo)) { alert("class info at index " + i + " missing attribute " + key); return; } } } whenOnSIS.addClasses(info); }; div.appendChild(textarea); div.appendChild(button); document.body.appendChild(div); } whenOnSIS.deleteClassesFromCart = function() { if (GM_getValue('setClassesImmediately', false)) { // next time we go to url don't auto; only do once GM_setValue('setClassesImmediately', false); if (GM_getValue('clearClasses', false)) { GM_setValue('clearClasses', false); if (window.parent.location.hash.indexOf('#cart') === 0) { // this should always be the case whenOnSIS.waitFor(function() { return document.querySelector('th.PSLEVEL1GRIDCOLUMNHDR') !== null; }, whenOnSIS.deleteCourse); } } } } whenOnSIS.deleteCourse = function(currLen) { if (currLen === undefined) { currLen = whenOnSIS.getTableLength(); } // argh https://stackoverflow.com/a/42907951/9911203 var trashCan = document.querySelector('img[src="/cs/csprd/cache/PS_DELETE_ICN_1.gif"]'); if (trashCan === null) { // done deleting classes from cart window.parent.addClassesToCart(); return; } trashCan.click(); var len; whenOnSIS.waitFor(function() { if (document.querySelector('img[src="/cs/csprd/cache/PS_DELETE_ICN_1.gif"]') === null) return true; len = whenOnSIS.getTableLength(); if (len === currLen) return false; return true; }, function() { whenOnSIS.deleteCourse(len); }); } whenOnSIS.getTableLength = function() { return document.querySelector('table.PSLEVEL1GRIDNBO').children[0].children.length; } function whenOnMyWebsite() { // document.getElementById('nouserscript').parentElement.removeChild(document.getElementById('nouserscript')) // const scriptBox = document.createElement('textarea'); // scriptBox.rows = "7"; // scriptBox.cols = "40"; const left = document.getElementById('left'); // left.appendChild(scriptBox); let e = document.getElementById("no-userscript"); if (e !== null) { e.remove(); } const setClassesButton = document.createElement('button'); setClassesButton.className = "btn btn-primary mr-2"; setClassesButton.innerHTML = 'replace cart with these'; const addClassesButton = document.createElement('button'); addClassesButton.className = "btn btn-primary"; addClassesButton.innerHTML = 'add these to cart'; setClassesButton.onclick = function() { GM_setValue('clearClasses', true); GM_setValue('setClassesImmediately', true); window.open(getEnrollmentCartURL); }; addClassesButton.onclick = function() { GM_setValue('clearClasses', false); GM_setValue('setClassesImmediately', true); window.open(baseURL + searchSearch); }; const p = document.createElement('p'); p.className = "hide-in-small-screen"; // p.appendChild(setClassesButton); // not having this button anymore because it doesn't work now p.appendChild(addClassesButton); left.appendChild(p); function updateClasses() { var val = getInfoForAddClasses("ALL"); // is this a good idea? does it matter? should it be ASE? GM_setValue('classes', val); // scriptBox.value = val; } document.addEventListener('updateClasses', updateClasses, false); updateClasses(); function getInfoForAddClasses(career) { var schedule = top_schedules[scheduleIndex].schedule; var classes = []; for (let i = 0; i < schedule.length; i++) { const current_course = courses[i]; const classNums = schedule[i].map(section_id => sections_by_id[section_id].class_num); classes.push({ course_num: current_course.course_num, classNums: classNums, title: current_course.title, }); } return JSON.stringify({ term_code: term_code, career: career, classes: classes }); } }