├── .gitignore ├── DB ├── DBManage.js ├── NewWharton.json ├── OldWharton.json ├── addEngReq.js ├── addWharReq.js ├── engreq.json └── wharreq.json ├── PCS Dev Setup.md ├── PCSangular.js ├── README.md ├── Screenshot.png ├── auto.js ├── factories.js ├── functions.js ├── importSched.js ├── index.js ├── like_button.js ├── loadCourses.js ├── loadRevs.js ├── opendata.js ├── package.json ├── parse.js ├── pcn.js ├── plugins ├── angular-local-storage.min.js ├── angular-tooltips.min.js ├── html2canvas.js ├── jquery.leanModal.min.js ├── jquery.tooltipster.min.js └── sweetalert.min.js ├── public ├── Import.png ├── Logo.png ├── css │ ├── blue_heart_2.png │ ├── filter_a.png │ ├── filter_b.png │ ├── graphic.svg │ ├── index.css │ ├── plugins │ │ ├── angular-tooltips.min.css │ │ └── sweetalert.css │ ├── sparkles.png │ └── venmo.png ├── favicon.ico ├── index.html ├── js │ ├── PCSangular.js │ ├── after_load.js │ ├── dropdown.js │ ├── factories.js │ ├── functions.js │ ├── importSched.js │ ├── modal_adjustments.js │ ├── plugins │ │ ├── angular-local-storage.min.js │ │ ├── angular-tooltips.min.js │ │ ├── html2canvas.js │ │ ├── jquery.leanModal.min.js │ │ ├── jquery.tooltipster.min.js │ │ └── sweetalert.min.js │ └── ui_adjustment.js └── keen.svg ├── react_components └── dropdown.js ├── reqFunctions.js ├── ui_adjustment.js └── views ├── 404.html ├── 50x.html ├── auto.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | /config.js 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | /node_modules/ 31 | 32 | DB Backup/ 33 | Stormpath\ Backup/ 34 | Data/ 35 | Server Files/ 36 | Logs/ 37 | Analytics/ 38 | Keen\ Export/ 39 | letsencrypt/ 40 | package-lock.json 41 | 42 | .DS_Store 43 | .idea/ -------------------------------------------------------------------------------- /DB/DBManage.js: -------------------------------------------------------------------------------- 1 | /* 2 | Main script for automating data scraping. 3 | Used with the OpenData API for registrar requests (in conjunction with opendata.js), 4 | as well as with the PCR API. 5 | 6 | Run as a script like so: 7 | 8 | node DBManage.js registrar MEAM // Run for a single department 9 | node DBManage.js registrar 0 30 // Run for a set of depts by index 10 | node DBManage.js registrar 50 // Run for all depts starting at index 11 | node DBManage.js review CIS 12 | */ 13 | 14 | var request = require("request"); 15 | var fs = require("fs"); 16 | 17 | var config; 18 | try { 19 | config = require('../config.js'); 20 | } catch (err) { // If there is no config file 21 | config = {}; 22 | config.requestAB2 = process.env.REQUESTAB2; 23 | config.requestAT2 = process.env.REQUESTAT2; 24 | config.PCRToken = process.env.PCRTOKEN; 25 | } 26 | 27 | var currentTerm = '2019A'; 28 | var currentRev = '2017CRev'; 29 | var deptList = ["AAMW", "ACCT", "AFRC", "AFST", "ALAN", "AMCS", "ANCH", "ANEL", "ANTH", "ARAB", "ARCH", "ARTH", "ASAM", "ASTR", "BCHE", "BDS", "BE", "BENF", "BENG", "BEPP", "BIBB", "BIOE", "BIOL", "BIOM", "BIOT", "BMB", "BMIN", "BSTA", "CAMB", "CBE", "CHEM", "CHIN", "CIMS", "CIS", "CIT", "CLST", "COGS", "COML", "COMM", "CPLN", "CRIM", "DEMG", "DENT", "DPED", "DPRD", "DRST", "DTCH", "DYNM", "EALC", "EAS", "ECON", "EDUC", "EEUR", "ENGL", "ENGR", "ENM", "ENMG", "ENVS", "EPID", "ESE", "FNAR", "FNCE", "FOLK", "FREN", "GAFL", "GAS", "GCB", "GEOL", "GREK", "GRMN", "GSWS", "GUJR", "HCIN", "HCMG", "HEBR", "HIND", "HIST", "HPR", "HSOC", "HSPV", "HSSC", "IMUN", "INTG", "INTL", "INTR", "INTS", "IPD", "ITAL", "JPAN", "JWST", "KORN", "LALS", "LARP", "LATN", "LAW", "LAWM", "LGIC", "LGST", "LING", "LSMP", "MATH", "MCS", "MEAM", "MED", "MGEC", "MGMT", "MKTG", "MLA", "MLYM", "MMP", "MSCI", "MSE", "MSSP", "MTR", "MUSA", "MUSC", "NANO", "NELC", "NETS", "NGG", "NPLD", "NSCI", "NURS", "OIDD", "PERS", "PHIL", "PHRM", "PHYS", "PPE", "PREC", "PRTG", "PSCI", "PSYC", "PUBH", "PUNJ", "REAL", "REG", "RELS", "ROML", "RUSS", "SAST", "SCND", "SKRT", "SLAV", "SOCI", "SPAN", "STAT", "STSC", "SWRK", "TAML", "TELU", "THAR", "TURK", "URBS", "URDU", "VBMS", "VCSN", "VCSP", "VIPR", "VISR", "VLST", "VMED", "VPTH", "WH", "WHCP", "WHG", "WRIT", "YDSH"]; 30 | var maxIndex = deptList.length; 31 | 32 | var opendata = require('../opendata.js')(95); 33 | var lastRequestTime = 0; 34 | 35 | var source = process.argv[2].toLowerCase(); // registrar or review 36 | var index = Number(process.argv[3]); // Can be a number or a dept code 37 | var endindex; 38 | try { 39 | endindex = Number(process.argv[4]) + 1; // If there's a second number, end at that number 40 | } catch(err) {} 41 | var limit = false; 42 | if (isNaN(index)) { // If we're doing a specific department (not a number, but rather a deptcode) 43 | limit = 1; 44 | index = deptList.indexOf(process.argv[3].toUpperCase()); 45 | } 46 | if (isNaN(endindex)) { // If there is a first number but not a second, run to the end 47 | endindex = maxIndex; 48 | } 49 | 50 | var thedept; 51 | if (source === 'registrar') { 52 | if (limit) { 53 | thedept = deptList[index]; 54 | PullRegistrar(thedept); // Single dept 55 | } else { 56 | for (var i = index; i < endindex; i++) { 57 | thedept = deptList[i]; 58 | PullRegistrar(thedept); 59 | } 60 | } 61 | } else if (source === "review") { 62 | if (limit) { 63 | PullReview(index); 64 | } else { 65 | for (var i = index; i < endindex; i++) { 66 | PullReview(i); 67 | } 68 | } 69 | } 70 | return "done"; 71 | 72 | function PullRegistrar(thedept) { 73 | console.log('Start', thedept); 74 | // Send the request 75 | var baseURL = 'https://esb.isc-seo.upenn.edu/8091/open_data/course_section_search?number_of_results_per_page=400&term='+currentTerm+'&course_id='+thedept; 76 | if (!thedept) {return;} 77 | 78 | lastRequestTime = opendata.RateLimitReq(baseURL, 'registrar', {}, lastRequestTime, 1); 79 | } 80 | 81 | function PullReview(index) { 82 | var thedept = deptList[index]; 83 | console.log(('PCR Rev Spit: '+thedept)); 84 | if (!thedept) {return;} 85 | request({ 86 | // Get raw data 87 | uri: 'http://api.penncoursereview.com/v1/depts/'+thedept+'/reviews?token='+config.PCRToken 88 | }, function(error, response, body) { 89 | console.log('Received'); 90 | var deptReviews = {}; 91 | try { 92 | deptReviews = JSON.parse(body).result.values; 93 | } catch(err) { 94 | console.log(thedept); 95 | console.log(err); 96 | return; 97 | } 98 | var resp = {}; 99 | for(var rev in deptReviews) { if(deptReviews.hasOwnProperty(rev)) { 100 | // Iterate through each review 101 | var sectionIDs = deptReviews[rev].section.aliases; 102 | for(var alias in sectionIDs) { 103 | if (sectionIDs[alias].split('-')[0] === thedept) { // Only create an entry for the course in this department 104 | // Get data 105 | var courseNum = sectionIDs[alias].split('-')[1]; 106 | var instID = deptReviews[rev].instructor.id.replace('-', ' ').split(' ')[1].replace(/--/g, '. ').replace(/-/g, ' '); 107 | // var instName = deptReviews[rev].instructor.name; 108 | var courseQual = Number(deptReviews[rev].ratings.rCourseQuality); 109 | var courseDiff = Number(deptReviews[rev].ratings.rDifficulty); 110 | var courseInst = Number(deptReviews[rev].ratings.rInstructorQuality); 111 | 112 | var revID = Number(deptReviews[rev].id.split("-")[0]); 113 | 114 | if (typeof resp[courseNum] === 'undefined') {resp[courseNum] = {};} 115 | if (typeof resp[courseNum][instID] === 'undefined') {resp[courseNum][instID] = [];} 116 | var entry = resp[courseNum][instID]; 117 | entry.push({ 118 | 'cQ': (courseQual || 0), 119 | 'cD': (courseDiff || 0), 120 | 'cI': (courseInst || 0) 121 | }); 122 | if (typeof resp[courseNum].Recent === 'undefined') {resp[courseNum].Recent = {'lastrev': 0, 'revs': []};} 123 | if (resp[courseNum].Recent.lastrev < revID) { 124 | resp[courseNum].Recent.lastrev = revID; 125 | resp[courseNum].Recent.revs = []; 126 | } 127 | if (resp[courseNum].Recent.lastrev <= revID) { 128 | resp[courseNum].Recent.revs.push({ 129 | 'cQ': (courseQual || 0), 130 | 'cD': (courseDiff || 0), 131 | 'cI': (courseInst || 0) 132 | }); 133 | } 134 | } 135 | } 136 | }} 137 | /* This JSON has the following form (using MEAM 101 as an example) 138 | { 139 | "MEAM 101": { 140 | "1234-Fiene": [ 141 | {"cQ": 4, "cD": 3.9, "cI": 4}, 142 | {"cQ": 3.9, "cD": 4, "cI": 3.9} 143 | ], 144 | "4321-Wabiszewski": [ 145 | {"cQ": 4, "cD": 3.9, "cI": 4} 146 | ] 147 | } 148 | } 149 | */ 150 | // This part computes average values and replaces the full data 151 | for (var course in resp) { if (resp.hasOwnProperty(course)) { 152 | var courseSumQ = 0; 153 | var courseSumD = 0; 154 | var courseSumI = 0; 155 | var revcount = 0; 156 | var recentQ = 0; 157 | var recentD = 0; 158 | var recentI = 0; 159 | var recentrevcount = 0; 160 | for (var inst in resp[course]) { if (resp[course].hasOwnProperty(inst)) { 161 | var review; 162 | var thisrev; 163 | if (inst !== 'Recent') { 164 | var instSumQ = 0; 165 | var instSumD = 0; 166 | var instSumI = 0; 167 | for (review in resp[course][inst]) { if (resp[course][inst].hasOwnProperty(review)) { 168 | thisrev = resp[course][inst][review]; 169 | instSumQ += thisrev.cQ; 170 | instSumD += thisrev.cD; 171 | instSumI += thisrev.cI; 172 | revcount++; 173 | }} 174 | // Get average ratings for each instructor for a given class 175 | var instAvgQual = Math.round(100 * instSumQ / resp[course][inst].length)/100; 176 | var instAvgDiff = Math.round(100 * instSumD / resp[course][inst].length)/100; 177 | var instAvgInst = Math.round(100 * instSumI / resp[course][inst].length)/100; 178 | resp[course][inst] = { 179 | 'cQ': instAvgQual, 180 | 'cD': instAvgDiff, 181 | 'cI': instAvgInst 182 | }; 183 | courseSumQ += instSumQ; 184 | courseSumD += instSumD; 185 | courseSumI += instSumI; 186 | } else { 187 | for (review in resp[course].Recent.revs) {if (resp[course].Recent.revs.hasOwnProperty(review)) { 188 | thisrev = resp[course].Recent.revs[review]; 189 | recentQ += thisrev.cQ; 190 | recentD += thisrev.cD; 191 | recentI += thisrev.cI; 192 | recentrevcount++; 193 | }} 194 | } 195 | }} 196 | if (!revcount) {revcount = 1;} 197 | if (!recentrevcount) {recentrevcount = 1;} 198 | // Get average of average instructor ratings for a given class 199 | var courseAvgQual = Math.round(100 * courseSumQ / revcount) /100; 200 | var courseAvgDiff = Math.round(100 * courseSumD / revcount) /100; 201 | var courseAvgInst = Math.round(100 * courseSumI / revcount) /100; 202 | resp[course].Total = { 203 | 'cQ': courseAvgQual, 204 | 'cD': courseAvgDiff, 205 | 'cI': courseAvgInst 206 | }; 207 | var recentAvgQual = Math.round(100 * recentQ / recentrevcount) /100; 208 | var recentAvgDiff = Math.round(100 * recentD / recentrevcount) /100; 209 | var recentAvgInst = Math.round(100 * recentI / recentrevcount) /100; 210 | resp[course].Recent = { 211 | 'cQ': recentAvgQual, 212 | 'cD': recentAvgDiff, 213 | 'cI': recentAvgInst 214 | }; 215 | }} 216 | 217 | fs.writeFile('../Data/'+currentRev+'/'+thedept+'.json', JSON.stringify(resp), function (err) { 218 | if (err) { 219 | console.log(index+' '+thedept+' '+err); 220 | } else { 221 | console.log('It\'s saved! '+ index + ' ' + thedept); 222 | } 223 | }); 224 | }); 225 | return 0; 226 | } -------------------------------------------------------------------------------- /DB/addEngReq.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This script is used to easily add entries into the engreq.json file 4 | node addEngReq.js natsci t CAMB 5 | node addEngReq.js ss t [ECON,101,102 6 | node addEngReq.js writ h WRIT-014 7 | 8 | */ 9 | var fs = require('fs'); 10 | var engreq = require('./engreq.json'); // pull existing info 11 | var code = process.argv[4].toUpperCase(); 12 | 13 | var codearray = []; 14 | if (code[0] === '[') { // If we want to add multiple classes in a dept 15 | code = code.split('[')[1]; 16 | var codesplit = code.split(','); 17 | var dept = codesplit[0]; // Get the dept 18 | for (var i = 1; i < codesplit.length; i++) { // create array of idDashed's 19 | codearray.push(dept+'-'+codesplit[i]); 20 | } 21 | } else { 22 | codearray[0] = code; 23 | } 24 | var req = process.argv[2].toLowerCase(); 25 | var tf = (process.argv[3][0].toLowerCase() === 't'); 26 | 27 | for (var i = 0; i < codearray.length; i++) { 28 | if (!engreq[codearray[i]]) { // If the dept/course doesn't exist in the record, make an entry 29 | engreq[codearray[i]] = {}; 30 | } 31 | engreq[codearray[i]][req] = tf; // Set the value 32 | if (req === 'tbs') { // TBS classes by definition do not count for eng 33 | engreq[codearray[i]].eng = false; 34 | } 35 | if (req === 'writ') { // For writing, instead of true or false, assume that a record means true and specifiy the req it fills 36 | if (process.argv[3][0].toLowerCase() === 'h') {engreq[codearray[i]].writ = true; engreq[codearray[i]].hum = true;} 37 | if (process.argv[3][0].toLowerCase() === 's') {engreq[codearray[i]].writ = true; engreq[codearray[i]].ss = true;} 38 | } 39 | } 40 | console.log(engreq); 41 | fs.writeFile('./engreq.json', JSON.stringify(engreq), function (err) { // Write to file 42 | if (err) { 43 | console.log("error: " + err); 44 | } 45 | }); -------------------------------------------------------------------------------- /DB/addWharReq.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var oldd = require('./OldWharton.json'); 4 | var newd = require('./NewWharton.json'); 5 | 6 | var obj2 = {}; 7 | 8 | 9 | var GEDOldmap = { 10 | 'SS': 'WSST', 11 | 'SandT': 'WSAT', 12 | 'LAC': 'WLAC' 13 | }; 14 | 15 | for (var c in oldd.data) { 16 | var thisc = oldd.data[c]; 17 | var idDashed = thisc[0] + '-' + thisc[1].toString().padStart(3,'0'); 18 | if (!obj2[idDashed]) {obj2[idDashed] = {};} 19 | 20 | if (thisc[3] === 'Yes ') { 21 | obj2[idDashed].global = 'WGLO'; 22 | } 23 | var thisGED = GEDOldmap[thisc[4]]; 24 | obj2[idDashed].GEDOld = thisGED; 25 | } 26 | 27 | var ccpmap = { 28 | 'No -': 'N', 29 | 'Yes -': 'WCCY', 30 | 'See Advisor': 'WCCS', 31 | 'Yes - CDUS': 'WCCC' 32 | }; 33 | 34 | var GEDNewmap = { 35 | 'H': 'WNHR', 36 | 'NSME': 'WNNS', 37 | 'SS': 'WNSS', 38 | 'FGE': 'WNFR', 39 | 'URE': 'WURE', 40 | 'See Advisor': 'WNSA' 41 | }; 42 | 43 | for (c in newd.data){ 44 | var thisc = newd.data[c]; 45 | var idDashed = thisc[0] + '-' + thisc[1].toString().padStart(3,'0'); 46 | var thisGED = GEDNewmap[thisc[3]]; 47 | var thisCCP = ccpmap[thisc[4]]; 48 | // console.log(idDashed, thisGED, thisCCP) 49 | if (!obj2[idDashed]) {obj2[idDashed] = {};} 50 | if (thisCCP !== 'N') { 51 | obj2[idDashed].CCPNew = thisCCP; 52 | } 53 | obj2[idDashed].GEDNew = thisGED; 54 | } 55 | // console.log(obj2) 56 | 57 | fs.writeFile('./wharreq.json', JSON.stringify(obj2), function (err) { // Write to file 58 | if (err) { 59 | console.log("error: " + err); 60 | } 61 | }); -------------------------------------------------------------------------------- /DB/engreq.json: -------------------------------------------------------------------------------- 1 | {"CIS-160":{"math":true,"eng":false},"CIS-261":{"math":true,"eng":false},"CIS-262":{"math":true,"eng":false},"EAS-205":{"math":true},"ENM":{"math":true,"eng":false},"ESE-301":{"math":true,"eng":false},"ESE-302":{"math":true,"eng":false},"MATH":{"math":true},"MATH-115":{"math":false,"nocred":true},"MATH-122":{"math":false},"MATH-123":{"math":false},"MATH-130":{"math":false},"MATH-150":{"math":false},"MATH-151":{"math":false},"MATH-170":{"math":false,"nocred":true},"MATH-172":{"math":false},"MATH-174":{"math":false},"MATH-180":{"math":false},"MATH-210":{"math":false},"MATH-212":{"math":false},"MATH-220":{"math":false},"MATH-475":{"math":false},"PHIL-005":{"math":true,"hum":false},"PHIL-006":{"math":true,"hum":false},"STAT-430":{"math":true},"STAT-431":{"math":true},"STAT-432":{"math":true},"STAT-433":{"math":true},"ASTR-111":{"natsci":true},"ASTR-211":{"natsci":true},"ASTR-212":{"natsci":true},"ASTR-250":{"natsci":true},"ASTR-392":{"natsci":true},"ASTR-410":{"natsci":true},"ASTR-411":{"natsci":true},"ASTR-412":{"natsci":true},"BCHE":{"natsci":true},"BE-305":{"natsci":true},"BE-505":{"natsci":true},"BE-513":{"natsci":true,"eng":false},"BIBB-010":{"natsci":false},"BIBB-050":{"natsci":false},"BIBB-060":{"natsci":false},"BIBB-160":{"natsci":false},"BIBB-227":{"natsci":false},"BIBB":{"natsci":true},"BIOL-091":{"natsci":true},"BIOL-092":{"natsci":true},"BIOL-130":{"natsci":false},"BIOL-446":{"natsci":false},"BIOL-544":{"natsci":false},"CAMB":{"natsci":true},"CHEM":{"natsci":true},"CHEM-010":{"natsci":false},"CHEM-011":{"natsci":false},"CHEM-012":{"natsci":false},"CHEM-022":{"natsci":false},"CHEM-023":{"natsci":false},"CIS-398":{"natsci":true},"ESE-112":{"natsci":true},"EAS-210":{"natsci":true},"GCB":{"natsci":true},"GEOL-109":{"natsci":true},"GEOL-130":{"natsci":true},"MSE-221":{"natsci":true,"eng":false},"MEAM-110":{"natsci":true,"eng":false},"MEAM-147":{"natsci":true,"eng":false},"PHYS-050":{"natsci":true},"PHYS-051":{"natsci":true},"PHYS-093":{"natsci":true},"PHYS-094":{"natsci":true},"PHYS-140":{"natsci":true},"PHYS-141":{"natsci":true},"PHYS-314":{"natsci":false},"PHYS-360":{"natsci":false},"PHYS-500":{"natsci":false},"BE-280":{"eng":false,"tbs":true},"BE-303":{"eng":false},"BE-503":{"eng":false,"tbs":true},"CIS-100":{"eng":false,"nocred":true},"CIS-101":{"eng":false,"nocred":true},"CIS-105":{"eng":false},"CIS-106":{"eng":false,"hum":true},"CIS-125":{"eng":false,"tbs":true},"CIS-260":{"eng":false},"CIS-313":{"eng":false},"CIS-355":{"eng":false,"tbs":true},"CIS-590":{"eng":false,"tbs":true},"EAS":{"eng":false},"BE":{"eng":true},"CIS":{"eng":true},"IPD-509":{"eng":false,"tbs":true,"hum":true},"IPD-549":{"eng":false,"tbs":true},"MCIT":{"eng":false,"nocred":true},"ESE":{"eng":true},"IPD":{"eng":true},"MEAM":{"eng":true},"MSE":{"eng":true},"EAS-009":{"tbs":true,"eng":false},"EAS-111":{"tbs":true,"eng":false},"EAS-125":{"tbs":true,"eng":false},"EAS-280":{"tbs":true,"eng":false},"EAS-281":{"tbs":true,"eng":false},"EAS-282":{"tbs":true,"eng":false},"EAS-285":{"tbs":true,"eng":false},"EAS-290":{"tbs":true,"eng":false},"EAS-301":{"tbs":true,"eng":false},"EAS-306":{"tbs":true,"eng":false},"EAS-345":{"tbs":true,"eng":false},"EAS-346":{"tbs":true,"eng":false},"EAS-348":{"tbs":true,"eng":false},"EAS-349":{"tbs":true,"eng":false},"EAS-400":{"tbs":true,"eng":false},"EAS-401":{"tbs":true,"eng":false},"EAS-402":{"tbs":true,"eng":false},"EAS-403":{"tbs":true,"eng":false},"EAS-445":{"tbs":true,"eng":false},"EAS-446":{"tbs":true,"eng":false},"EAS-448":{"tbs":true,"eng":false},"EAS-449":{"tbs":true,"eng":false},"EAS-500":{"tbs":true,"eng":false},"EAS-501":{"tbs":true,"eng":false},"EAS-502":{"tbs":true,"eng":false},"EAS-507":{"tbs":true,"eng":false},"EAS-510":{"tbs":true,"eng":false},"EAS-512":{"tbs":true,"eng":false},"EAS-545":{"tbs":true,"eng":false},"EAS-546":{"tbs":true,"eng":false},"EAS-548":{"tbs":true,"eng":false},"EAS-549":{"tbs":true,"eng":false},"EAS-590":{"tbs":true,"eng":false},"EAS-595":{"tbs":true,"eng":false},"IPD-545":{"tbs":true,"eng":false},"MEAM-399":{"tbs":true,"eng":false},"MSE-266":{"tbs":true,"eng":false},"WRIT-002":{"writ":true,"hum":true},"WRIT-009":{"writ":true,"hum":true},"WRIT-010":{"writ":true,"hum":true},"WRIT-011":{"writ":true,"hum":true},"WRIT-012":{"writ":true,"hum":true},"WRIT-013":{"writ":true,"hum":true},"WRIT-014":{"writ":true,"hum":true},"WRIT-015":{"writ":true,"hum":true},"WRIT-030":{"writ":true,"hum":true},"WRIT-036":{"writ":true,"hum":true},"WRIT-039":{"writ":true,"hum":true},"WRIT-041":{"writ":true,"hum":true},"WRIT-042":{"writ":true,"hum":true},"WRIT-047":{"writ":true,"hum":true},"WRIT-049":{"writ":true,"hum":true},"WRIT-056":{"writ":true,"hum":true},"WRIT-058":{"writ":true,"hum":true},"WRIT-066":{"writ":true,"hum":true},"WRIT-067":{"writ":true,"hum":true},"WRIT-068":{"writ":true,"hum":true},"WRIT-073":{"writ":true,"hum":true},"WRIT-082":{"writ":true,"hum":true},"WRIT-083":{"writ":true,"hum":true},"WRIT-084":{"writ":true,"hum":true},"WRIT-086":{"writ":true,"hum":true},"WRIT-087":{"writ":true,"hum":true},"WRIT-091":{"writ":true,"hum":true},"WRIT-125":{"writ":true,"hum":true},"WRIT-016":{"writ":true,"ss":true},"WRIT-028":{"writ":true,"ss":true},"WRIT-029":{"writ":true,"ss":true},"WRIT-035":{"writ":true,"ss":true},"WRIT-037":{"writ":true,"ss":true},"WRIT-048":{"writ":true,"ss":true},"WRIT-050":{"writ":true,"ss":true},"WRIT-055":{"writ":true,"ss":true},"WRIT-059":{"writ":true,"ss":true},"WRIT-075":{"writ":true,"ss":true},"WRIT-076":{"writ":true,"ss":true},"WRIT-077":{"writ":true,"ss":true},"WRIT-085":{"writ":true,"ss":true},"WRIT-088":{"writ":true,"ss":true},"WRIT-089":{"writ":true,"ss":true},"WRIT-092":{"writ":true,"ss":true},"WRIT-021":{"writ":true},"WRIT-022":{"writ":true},"WRIT-024":{"writ":true},"WRIT-034":{"writ":true},"WRIT-038":{"writ":true},"WRIT-040":{"writ":true},"WRIT-057":{"writ":true},"WRIT-060":{"writ":true},"WRIT-065":{"writ":true},"WRIT-074":{"writ":true},"WRIT-090":{"writ":true},"WRIT-135":{"writ":true},"ASTR-001":{"nocred":true},"CHEM-001":{"nocred":true},"EAS-503":{"nocred":true},"EAS-505":{"nocred":true},"EDUC":{"nocred":true},"MEAM-091":{"nocred":true},"MEAM-092":{"nocred":true},"MEAM-093":{"nocred":true},"MEAM-094":{"nocred":true},"MEAM-095":{"nocred":true},"MSCI":{"nocred":true},"NSCI":{"nocred":true},"NSCI-102":{"nocred":false},"NSCI-201":{"nocred":false},"NSCI-202":{"nocred":false},"NSCI-401":{"nocred":false},"NSCI-402":{"nocred":false},"DYNM":{"nocred":true},"MED":{"nocred":true},"COMM":{"ss":true},"CRIM":{"ss":true},"ECON":{"ss":true},"GSWS":{"ss":true},"HSOC":{"ss":true},"INTR":{"ss":true},"LING":{"ss":true},"PPE":{"ss":true},"PSCI":{"ss":true},"PSYC":{"ss":true},"SOCI":{"ss":true},"STSC":{"ss":true},"URBS":{"ss":true},"BEPP-201":{"ss":true},"BEPP-203":{"ss":true},"BEPP-212":{"ss":true},"BEPP-250":{"ss":true},"BEPP-288":{"ss":true},"BEPP-289":{"ss":true},"EAS-203":{"ss":true},"EAS-303":{"ss":true},"FNCE-101":{"ss":true},"FNCE-103":{"ss":true},"HSSC":{"ss":true,"hum":true},"LGST-100":{"ss":true},"LGST-101":{"ss":true},"LGST-210":{"ss":true},"LGST-215":{"ss":true},"LGST-220":{"ss":true},"NURS-098":{"ss":true},"NURS-313":{"ss":true},"NURS-315":{"ss":true},"NURS-316":{"ss":true},"NURS-317":{"ss":true},"NURS-330":{"ss":true},"NURS-331":{"ss":true},"NURS-333":{"ss":true},"ANTH":{"ss":false,"hum":true},"ANCH":{"hum":true},"ANEL":{"hum":true},"ARTH":{"hum":true},"CLST":{"hum":true},"COML":{"hum":true},"EALC":{"hum":true},"ENGL":{"hum":true},"FNAR":{"hum":true},"FOLK":{"hum":true},"GRMN":{"hum":true},"DTCH":{"hum":true},"SCND":{"hum":true},"HIST":{"hum":true},"JWST":{"hum":true},"LALS":{"hum":true},"MUSC":{"hum":true},"NELC":{"hum":true},"PHIL":{"hum":true},"RELS":{"hum":true},"FREN":{"hum":true},"ITAL":{"hum":true},"PRTG":{"hum":true},"SPAN":{"hum":true},"ROML":{"hum":true},"EEUR":{"hum":true},"RUSS":{"hum":true},"SLAV":{"hum":true},"SARS":{"hum":true},"SAST":{"hum":true},"THAR":{"hum":true},"VLST":{"hum":true},"ARCH-101":{"hum":true},"ARCH-201":{"hum":true},"ARCH-202":{"hum":true},"ARCH-301":{"hum":true},"ARCH-302":{"hum":true},"ARCH-401":{"hum":true},"ARCH-402":{"hum":true},"ARCH-403":{"hum":true},"ARCH-411":{"hum":true},"ARCH-412":{"hum":true},"IPD-403":{"hum":true},"PHYS":{"natsci":true},"BIOL":{"natsci":true},"GEOL":{"natsci":true}} -------------------------------------------------------------------------------- /PCS Dev Setup.md: -------------------------------------------------------------------------------- 1 | # PCS Development Setup 2 | 3 | ### Please follow the steps below to set up PCS in your dev environment 4 | 5 | 1. Clone the PCS repo from https://github.com/benb116/PennCourseSearch 6 | 2. You will need the following keys to run PCS: 7 | * OpenData request authorization bearers and tokens (see https://esb.isc-seo.upenn.edu/8091/documentation/) 8 | * PCS supports using multiple API keys for OpenData to achieve more than 100 requests per minute. 9 | * Penn Course Review API token (see http://pennlabs.org/docs/pcr.html) 10 | * Keen IO keys (optional) (see https://keen.io/) 11 | * IFTTT key (optional) (see https://ifttt.com/) 12 | 3. In the top level of the directory, create your config.json file 13 | * The file should contain the following entries, filling in your values 14 | ``` 15 | var config = {}; 16 | 17 | config.requestAB = ['', '']; // OpenData Authorization bearers array 18 | config.requestAT = ['','']; // OpenData Authorization tokens array, order matches requestAB 19 | 20 | config.PCRToken = ''; 21 | 22 | config.KeenIOID = ''; 23 | config.KeenIOWriteKey = ''; 24 | 25 | config.IFTTTKey = ''; 26 | 27 | module.exports = config; 28 | ``` 29 | * Alternatively, set the corresponding environment variables (see "index.js" 30 | 4. `npm install` the packages listed in "package.json" 31 | 5. Create a top-level directory called "Data" and subdirectories with the name of the term for course data and review data, respectively 32 | * Naming convention: course data in directory "2019A" (Spring, 2019) and review data in "2017CRev" (review data that was published in the fall of 2017) 33 | 6. In the files "index.js", "DB/DBManage.js", and "public/js/PCSangular.js", find the lines that define `currentTerm` and `currentRev`. Replace the strings as necessary with the same strings from the previous step. 34 | 7. In "DB/DBManage.js", find the line `var opendata = require('../opendata.js')(N);` N is the number of requests to make per minute while caching OpenData information. Change N to be less than 100 (I usually set it to 95). 35 | 8. From the DB directory, run the following command to begin caching course data: `node DBManage.js registrar 0` 36 | * Change 0 to be a dept (e.g. MEAM) to cache a specific department 37 | * Append a second number to stop at a certain department index (e.g. `node DBManage.js registrar 0 20`) 38 | 9. Run the same command with `review` instead of `registrar` 39 | * It is recommended to run this command in chunks so as not to overload the PCR API 40 | 10. **IF YOU ARE SETTING UP PRODUCTION** make sure that the `NODE_ENV` environment variable is set to `'production'` 41 | 42 | This should be all you need to set up the environment. Run `node index.js` to start the server on port 3000 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PennCourseSearch 2 | 3 | Fed up with the bloated, inefficient, and slow excuse for an online portal that is Penn InTouch, I decided to make a cleaner and simpler way for Quakers to find classes and make schedules. While this is not a full replacement for Penn InTouch, it acts as an improvement of the "Course Search" and "Mock Schedules" features. 4 | 5 | Students can search departments, courses, and sections as well as descriptions and instructors. All of the data comes from the [Penn OpenData API](https://esb.isc-seo.upenn.edu/8091/documentation/) and [PennCourseReview API](http://pennlabs.org/docs/pcr.html). The server sorts and returns the requested information as JSON, which is then formatted client-side. Schedules are also created using OpenData information and the image is made using client-side JS. 6 | 7 | The server is written using NodeJS and the frontend with Angular. The app is currently hosted on ~~Heroku~~ ~~DigitalOcean~~ ~~Linode~~ Lightsail. 8 | 9 | [![Codacy Badge](https://api.codacy.com/project/badge/grade/2ba7031e553e4126a95ff0e47d65a161)](https://www.codacy.com/app/benb116/PennCourseSearch) 10 | 11 | Specific files you may be interested in: 12 | 13 | * [Server JS](https://github.com/benb116/PennCourseSearch/blob/master/index.js) 14 | * [JS (including Angular controller)](https://github.com/benb116/PennCourseSearch/tree/master/public/js) 15 | * [CSS](https://github.com/benb116/PennCourseSearch/blob/master/public/css/index.css) 16 | * [HTML with Angular directives](https://github.com/benb116/PennCourseSearch/blob/master/views/index.html) 17 | 18 | If you have questions, ideas, bug reports, or if you'd like to suggest a new subtitle, let me know. 19 | 20 | Screenshot! 21 | 22 | ![image](https://raw.githubusercontent.com/benb116/PennCourseSearch/master/Screenshot.png) -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/Screenshot.png -------------------------------------------------------------------------------- /auto.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var request = require('request'); 3 | var express = require('express'); 4 | var compression = require('compression'); 5 | 6 | var config; 7 | try { 8 | config = require('./config.js'); 9 | } catch (err) { // If there is no config file 10 | config = {}; 11 | config.requestAB = process.env.REQUESTAB; 12 | config.requestAT = process.env.REQUESTAT; 13 | config.PCRToken = process.env.PCRTOKEN; 14 | config.KeenIOID = process.env.KEEN_PROJECT_ID; 15 | config.KeenIOWriteKey = process.env.KEEN_WRITE_KEY; 16 | config.autotestKey = process.env.AUTOTESTKEY; 17 | } 18 | 19 | 20 | var app = express(); 21 | 22 | // Set express settings 23 | app.use(compression()); 24 | app.use('/js/plugins', express.static(path.join(__dirname, 'public/js/plugins'), { maxAge: 2628000000 })); 25 | app.use('/js', express.static(path.join(__dirname, 'public/js'), { maxAge: 0 })); 26 | app.use(express.static(path.join(__dirname, 'public'), { maxAge: 2628000000 })); 27 | 28 | console.log('Express initialized'); 29 | 30 | // Start the server 31 | app.listen(process.env.PORT || 3001, function(){ 32 | console.log("Node app is running. Better go catch it."); 33 | }); 34 | 35 | // Handle main page requests 36 | app.get('/auto/', function(req, res) { 37 | res.sendFile(path.join(__dirname+'/views/auto.html')); 38 | }); 39 | 40 | // Handle main page requests 41 | app.get('/auto/request', function(req, res) { 42 | console.log(req.query.course); 43 | var courses = req.query.course; 44 | // console.log(); 45 | return res.send(autoSched(courses)); 46 | }); 47 | 48 | var deptList = ["AAMW", "ACCT", "AFRC", "AFST", "ALAN", "AMCS", "ANCH", "ANEL", "ANTH", "ARAB", "ARCH", "ARTH", "ASAM", "ASTR", "BCHE", "BDS", "BE", "BENF", "BENG", "BEPP", "BIBB", "BIOE", "BIOL", "BIOM", "BIOT", "BMB", "BMIN", "BSTA", "CAMB", "CBE", "CHEM", "CHIN", "CIMS", "CIS", "CIT", "CLST", "COGS", "COML", "COMM", "CPLN", "CRIM", "DEMG", "DENT", "DPED", "DPRD", "DRST", "DTCH", "DYNM", "EALC", "EAS", "ECON", "EDUC", "EEUR", "ENGL", "ENGR", "ENM", "ENMG", "ENVS", "EPID", "ESE", "FNAR", "FNCE", "FOLK", "FREN", "GAFL", "GAS", "GCB", "GEOL", "GREK", "GRMN", "GSWS", "GUJR", "HCIN", "HCMG", "HEBR", "HIND", "HIST", "HPR", "HSOC", "HSPV", "HSSC", "IMUN", "INTG", "INTL", "INTR", "INTS", "IPD", "ITAL", "JPAN", "JWST", "KORN", "LALS", "LARP", "LATN", "LAW", "LAWM", "LGIC", "LGST", "LING", "LSMP", "MATH", "MCS", "MEAM", "MED", "MGEC", "MGMT", "MKTG", "MLA", "MLYM", "MMP", "MSCI", "MSE", "MSSP", "MTR", "MUSA", "MUSC", "NANO", "NELC", "NETS", "NGG", "NPLD", "NSCI", "NURS", "OIDD", "PERS", "PHIL", "PHRM", "PHYS", "PPE", "PREC", "PRTG", "PSCI", "PSYC", "PUBH", "PUNJ", "REAL", "REG", "RELS", "ROML", "RUSS", "SAST", "SCND", "SKRT", "SLAV", "SOCI", "SPAN", "STAT", "STSC", "SWRK", "TAML", "TELU", "THAR", "TURK", "URBS", "URDU", "VBMS", "VCSN", "VCSP", "VIPR", "VISR", "VLST", "VMED", "VPTH", "WH", "WHCP", "WHG", "WRIT", "YDSH"]; 49 | 50 | var meetData = []; 51 | for (var dept in deptList) { if (deptList.hasOwnProperty(dept)) { 52 | try { 53 | var thedept = deptList[dept]; 54 | var deptData = require('./Data/2018AMeet/'+thedept+'.json'); 55 | var secs = Object.keys(deptData); 56 | for (var i = 0; i < secs.length; i++) { 57 | var meetData = meetData.concat(deptData[secs[i]]); 58 | } 59 | } catch(err) { 60 | 61 | } 62 | }} 63 | 64 | // var args = process.argv; 65 | var courses = ['cis-110', 'meam-348','cis-120', 'math114', 'econ001', 'chem102', 'meam545', 'meam101', 'meam201', 'psyc001']; 66 | 67 | autoSched(courses); 68 | 69 | function autoSched(courses) { 70 | var allData = []; 71 | // Pull relevant meeting time info for the classes 72 | for (var i = 0; i < courses.length; i++) { 73 | var thisc = FormatID(courses[i]); 74 | courses[i] = thisc[0] + '-' + thisc[1]; 75 | var toAdd = meetData.filter(function(sec) { // Find all sections of the course 76 | return sec.course === courses[i]; 77 | }); 78 | allData = allData.concat(toAdd); 79 | } 80 | return run(allData); 81 | } 82 | 83 | function run(allData) { 84 | var coursesAdded = []; 85 | var datasched = {}; // Create object with each required course section type 86 | // { 87 | // MEAM-101-LAB: [MEAM-101-101, MEAM-101-102 ...], 88 | // ;.. 89 | // } 90 | allData.forEach(function(thissect) { 91 | if (thissect.open) { 92 | var thiscourseact = thissect.course + '-' + thissect.actType; 93 | if (!datasched[thiscourseact]) { 94 | datasched[thiscourseact] = []; 95 | } 96 | datasched[thiscourseact].push(thissect); 97 | } 98 | }); 99 | var d, twodarr; 100 | d = regenData(datasched); 101 | raw_twodarr = d[0]; // [MEAM-101-LEC, MEAM-101-LAB] 102 | raw_datasched = d[1];//[1, 4] 103 | 104 | raw_types = raw_twodarr.map(function(a) { 105 | return a[0]; 106 | }); 107 | 108 | var typekey = ['LEC', 'LAB', 'REC']; 109 | 110 | datasched = raw_datasched; 111 | twodarr = raw_twodarr; 112 | types = raw_types; 113 | 114 | for (var h = 0; h < raw_types.length; h++) { 115 | 116 | 117 | var anchor = datasched[types[h]][0]; 118 | if (!anchor) { 119 | console.log('noanch', types[h]); 120 | } 121 | console.log(types[h]); 122 | 123 | d = regenData(datasched); 124 | twodarr = d[0]; 125 | datasched = d[1]; 126 | typeNumObj = d[2]; 127 | var hold; 128 | 129 | for (var i = h+1; i < twodarr.length; i++) { 130 | indtorem = CompareAnchorToType(i); 131 | 132 | if (indtorem.length === twodarr[i][1]) { // No sections work with this anchor, move to next anchor index and try again. 133 | // anchorind++; 134 | console.log('full', types[h]); 135 | datasched[types[h]].splice(0, 1); 136 | i = twodarr.length; 137 | h--; 138 | hold = true; 139 | } else { 140 | 141 | 142 | var typeToRem = raw_twodarr[i][0]; 143 | datasched[typeToRem] = datasched[typeToRem].filter(function(a,e) { 144 | return (indtorem.indexOf(e) === -1); 145 | }); 146 | RemoveNonAsscSections(anchor); 147 | 148 | d = regenData(datasched); 149 | twodarr = d[0]; 150 | datasched = d[1]; 151 | } 152 | 153 | } 154 | if (hold) { 155 | hold = false; 156 | } else { 157 | coursesAdded.push(anchor.idDashed); 158 | } 159 | } 160 | console.log(coursesAdded); 161 | return coursesAdded; 162 | 163 | function CompareAnchorToType(i) { 164 | var indtorem = []; 165 | var type = raw_twodarr[i][0]; 166 | var numoftype = typeNumObj[type]; 167 | for (var j = 0; j < numoftype; j++) { 168 | var ameet = datasched[type][j]; 169 | if (ameet) { 170 | if (anchor.course !== ameet.course) { 171 | var isover = Overlap(ameet, anchor); 172 | if (isover) { 173 | console.log('conflict', ameet.idDashed); 174 | indtorem.push(j); 175 | } 176 | } 177 | } 178 | } 179 | return indtorem; 180 | } 181 | 182 | function RemoveNonAsscSections(anchor) { 183 | var all_anchor_assc = [anchor.assclec, anchor.assclab, anchor.asscrec]; 184 | for (var g = 0; g < 3; g++) { 185 | var thisassc = all_anchor_assc[g]; 186 | if (thisassc.length) { 187 | thisassclist = thisassc.map(function(a) { 188 | return a.subject + '-' + a.course_id + '-' + a.section_id; 189 | }); 190 | var thisassctype = anchor.course + '-' + typekey[g]; 191 | var dataschedassc = datasched[thisassctype].map(function(a) { 192 | return a.idDashed; 193 | }); 194 | 195 | datasched[thisassctype] = datasched[thisassctype].filter(function(a,e) { 196 | return (thisassclist.indexOf(dataschedassc[e]) > -1); 197 | }); 198 | } 199 | } 200 | } 201 | } 202 | 203 | function regenData(datasched) { 204 | var types = Object.keys(datasched); 205 | var nums = []; 206 | for (var i = 0; i < types.length; i++) { 207 | nums[i] = datasched[types[i]].length; 208 | } // Number of each sections of a specific type 209 | 210 | var twodarr = []; 211 | for (i = 0; i < types.length; i++) { 212 | twodarr[i] = [types[i], nums[i]]; 213 | } 214 | twodarr.sort(function(a,b) { 215 | return a[1] - b[1]; 216 | }); 217 | // Sorted double list of types and number of available sections of those types (low to hi) 218 | 219 | typeNumObj = {}; // Objectified 220 | for (i = 0; i < twodarr.length; i++) { 221 | typeNumObj[twodarr[i][0]] = twodarr[i][1]; 222 | } 223 | return [twodarr, datasched, typeNumObj]; 224 | } 225 | 226 | function Overlap(block1, block2) { 227 | // console.log(block1) 228 | var meet1 = block1.meetblk; 229 | var meet2 = block2.meetblk; 230 | var over = false; 231 | for (var i = 0; i < meet1.length; i++) { 232 | for (var j = 0; j < meet2.length; j++) { 233 | // console.log(1, meet1[i]) 234 | // console.log(2, meet2[j]) 235 | if (meet1[i].meetday === meet2[j].meetday) { 236 | over = check(meet1[i], meet2[j]); 237 | if (over) {return over;} 238 | } 239 | } 240 | } 241 | return over; 242 | } 243 | 244 | function check(block1, block2) { 245 | // Thank you to Stack Overflow user BC. for the function this is based on. 246 | // http://stackoverflow.com/questions/5419134/how-to-detect-if-two-divs-touch-with-jquery 247 | var y1 = block1.starthr; 248 | var b1 = block1.endhr; 249 | 250 | var y2 = block2.starthr; 251 | var b2 = block2.endhr; 252 | 253 | // This checks if the top of block 2 is lower down (higher value) than the bottom of block 1... 254 | // or if the top of block 1 is lower down (higher value) than the bottom of block 2. 255 | // In this case, they are not overlapping, so return false 256 | if (b1 <= y2 || b2 <= y1) { 257 | return false; 258 | } else { 259 | return true; 260 | } 261 | } 262 | 263 | function requestAsync(url) { 264 | return new Promise(function(resolve, reject) { 265 | request({ 266 | uri: url, 267 | method: "GET",headers: {"Authorization-Bearer": config.requestAB2, "Authorization-Token": config.requestAT2}, // Send authorization headers 268 | }, function(err, res, body) { 269 | if (err) { return reject(err); } 270 | return resolve([res, body]); 271 | }); 272 | }); 273 | } 274 | 275 | function FormatID(rawParam) { 276 | var searchParam = rawParam.replace(/ /g, "").replace(/-/g, "").replace(/:/g, ""); // Remove spaces, dashes, and colons 277 | var retArr = ['', '', '']; 278 | 279 | if (isFinite(searchParam[2])) { // If the third character is a number (e.g. BE100) 280 | splitTerms(2); 281 | } else if (isFinite(searchParam[3])) { // If the fourth character is a number (e.g. CIS110) 282 | splitTerms(3); 283 | } else if (isFinite(searchParam[4])) { // If the fifth character is a number (e.g. MEAM110) 284 | splitTerms(4); 285 | } else { 286 | retArr[0] = searchParam; 287 | } 288 | 289 | function splitTerms(n) { 290 | retArr[0] = searchParam.substr(0, n).toUpperCase(); 291 | retArr[1] = searchParam.substr(n, 3); 292 | retArr[2] = searchParam.substr(n+3, 3); 293 | } 294 | 295 | return retArr; 296 | } -------------------------------------------------------------------------------- /factories.js: -------------------------------------------------------------------------------- 1 | PCS.factory('PCR', function () { 2 | return function PCR(data) { 3 | angular.forEach(data, function (item) { 4 | var qFrac = item.revs.cQ / 4; 5 | var dFrac = item.revs.cD / 4; 6 | var iFrac = item.revs.cI / 4; 7 | item.pcrQShade = Math.pow(qFrac, 3) * 2; // This is the opacity of the PCR block 8 | item.pcrDShade = Math.pow(dFrac, 3) * 2; 9 | item.pcrIShade = Math.pow(iFrac, 3) * 2; 10 | if (qFrac < 0.50) { 11 | item.pcrQColor = 'black'; 12 | } else { 13 | item.pcrQColor = 'white'; 14 | } // It's hard to see white text on a light background 15 | if (dFrac < 0.50) { 16 | item.pcrDColor = 'black'; 17 | } else { 18 | item.pcrDColor = 'white'; 19 | } 20 | if (iFrac < 0.50) { 21 | item.pcrIColor = 'black'; 22 | } else { 23 | item.pcrIColor = 'white'; 24 | } 25 | item.revs.QDratio = item.revs.cQ - item.revs.cD; // This is my way of calculating if a class is "good and easy." R > 1 means good and easy, < 1 means bad and hard 26 | 27 | // Cleanup to keep incomplete data on the bottom; 28 | if (isNaN(item.revs.QDratio) || !isFinite(item.revs.QDratio)) { 29 | item.revs.QDratio = 0; 30 | } 31 | // the rating as a string - let's us make the actual rating something else and still show the correct number 32 | item.revs.cQT = item.revs.cQ.toFixed(2); 33 | if (item.revs.cQ === 0) { 34 | item.revs.cQT = ''; 35 | } 36 | item.revs.cDT = item.revs.cD.toFixed(2); 37 | if (item.revs.cD === 0) { 38 | item.revs.cDT = '';item.revs.QDratio = -100;item.revs.cD = 100; 39 | } 40 | }); 41 | return data; 42 | }; 43 | }); 44 | PCS.factory('UpdateCourseList', ['httpService', function (httpService) { 45 | var retObj = {}; 46 | retObj.getDeptCourses = function (dept, searchType, reqFilter, proFilter) { 47 | // Build the request URL 48 | var url = '/Search?searchType=' + searchType + '&resultType=deptSearch&searchParam=' + dept; 49 | if (reqFilter) { 50 | url += '&reqParam=' + reqFilter; 51 | } 52 | if (proFilter && proFilter !== 'noFilter') { 53 | url += '&proParam=' + proFilter; 54 | } 55 | ga('send', 'event', 'Search', 'deptSearch', dept); 56 | return httpService.get(url).then(function (data) { 57 | return data; 58 | }, function (err) { 59 | if (!err.config.timeout.$$state.value) { 60 | ErrorAlert(err); // If there's an error, show an error dialog 61 | } else { 62 | return []; 63 | } 64 | }); 65 | }; 66 | return retObj; 67 | }]); 68 | PCS.factory('UpdateSectionList', ['httpService', function (httpService) { 69 | var retObj = {}; 70 | retObj.getCourseSections = function (course) { 71 | ga('send', 'event', 'Search', 'numbSearch', course); 72 | return httpService.get('/Search?searchType=courseIDSearch&resultType=numbSearch&searchParam=' + course).then(function (data) { 73 | return data; 74 | }, function (err) { 75 | if (!err.config.timeout.$$state.value) { 76 | ErrorAlert(err); // If there's an error, show an error dialog 77 | } else { 78 | return []; 79 | } 80 | }); 81 | }; 82 | return retObj; 83 | }]); 84 | PCS.factory('UpdateSectionInfo', ['httpService', function (httpService) { 85 | var retObj = {}; 86 | retObj.getSectionInfo = function (section) { 87 | ga('send', 'event', 'Search', 'sectSearch', section); 88 | return httpService.get('/Search?searchType=courseIDSearch&resultType=sectSearch&searchParam=' + section).then(function (data) { 89 | return data; 90 | }, function (err) { 91 | if (!err.config.timeout.$$state.value) { 92 | ErrorAlert(err); // If there's an error, show an error dialog 93 | } else { 94 | return {}; 95 | } 96 | }); 97 | }; 98 | return retObj; 99 | }]); 100 | PCS.factory('UpdateSchedules', ['httpService', function (httpService) { 101 | var retObj = {}; 102 | retObj.getSchedData = function (secID, needLoc) { 103 | var url = '/Sched?courseID=' + secID; 104 | if (needLoc) { 105 | url += '&needLoc=1'; 106 | } 107 | return httpService.get(url).then(function (data) { 108 | return data; 109 | }, function (err) { 110 | if (!err.config.timeout.$$state.value) { 111 | ErrorAlert(err); // If there's an error, show an error dialog 112 | } else { 113 | return {}; 114 | } 115 | }); 116 | }; 117 | return retObj; 118 | }]); 119 | // This service keeps track of pending requests 120 | PCS.service('pendingRequests', function () { 121 | var pending = []; 122 | this.get = function () { 123 | return pending; 124 | }; 125 | this.add = function (request) { 126 | pending.push(request); 127 | }; 128 | this.remove = function (request) { 129 | pending = pending.filter(function (p) { 130 | return p.url !== request; 131 | }); 132 | }; 133 | this.cancelAll = function () { 134 | angular.forEach(pending, function (p) { 135 | p.canceller.resolve('cancelled'); 136 | }); 137 | pending.length = 0; 138 | }; 139 | }); 140 | // This service wraps $http to make sure pending requests are tracked 141 | PCS.service('httpService', ['$http', '$q', 'pendingRequests', function ($http, $q, pendingRequests) { 142 | this.get = function (url) { 143 | var canceller = $q.defer(); 144 | pendingRequests.add({ 145 | url: url, 146 | canceller: canceller 147 | }); 148 | //Request gets cancelled if the timeout-promise is resolved 149 | var requestPromise = $http.get(url, { timeout: canceller.promise }); 150 | //Once a request has failed or succeeded, remove it from the pending list 151 | requestPromise.finally(function () { 152 | pendingRequests.remove(url); 153 | }); 154 | return requestPromise; 155 | }; 156 | }]); -------------------------------------------------------------------------------- /functions.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('a[rel*=leanModal]').leanModal({ 3 | top: 70, 4 | closeButton: ".modal_close" 5 | }); // Define modal close button 6 | 7 | var subtitles = ["Free shipping on all items in your course cart", "You can press the back button, but you don't even need to.", "Invented by Benjamin Franklin in 1793", "Faster than you can say 'Wawa run'", "Classes sine PennCourseSearch vanae", 8 | // "On PennCourseSearch, no one knows you're Amy G.", 9 | "Designed by Ben in Speakman. Assembled in China.", "Help! I'm trapped in a NodeJS server! Bring Chipotle!", "With white sauce AND hot sauce", "Now 3.9% faster", "Number of squirrels online: 6", "Handling the business side since 2014", "Actually in touch"]; 10 | var paymentNoteBase = "https://venmo.com/?txn=pay&recipients=BenBernstein&amount=1&share=f&audience=friends¬e="; 11 | var paymentNotes = ["PennCourseSearch%20rocks%20my%20socks!", "Donation%20to%20PennInTouch%20Sucks,%20Inc.", "For%20your%20next%20trip%20to%20Wawa", "Offsetting%20the%20increased%20price%20of%20chicken%20over%20rice"]; 12 | $('#subtitle').html(subtitles[Math.floor(Math.random() * subtitles.length)]); // Show a random subtitle 13 | $('#paymentNote').attr('href', paymentNoteBase + paymentNotes[Math.floor(Math.random() * paymentNotes.length)]); // Use a random payment note 14 | 15 | if (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { 16 | // Doesn't look good on mobile, so tell the user 17 | setTimeout(function () { 18 | sweetAlert({ 19 | title: 'PCS Alert', 20 | text: "Your device seems to be too small. PCS does not currently support mobile viewing, but we're looking to add it soon!", 21 | type: 'warning' 22 | }); 23 | }, 300); 24 | } else { 25 | $.get('/Status').done(function (statusMessage) { 26 | if (statusMessage !== 'hakol beseder') { 27 | // If there is a status message from the server, it will be passed in the #StatusMessage block 28 | setTimeout(function () { 29 | sweetAlert({ 30 | title: 'PCS Alert', 31 | html: true, 32 | text: statusMessage, 33 | type: 'warning' 34 | }); 35 | }, 300); 36 | console.log(statusMessage); 37 | } else { 38 | if (localStorage.getItem('secondvisit')) { 39 | if (!localStorage.getItem('survey2018C')) { 40 | localStorage.setItem('survey2018C', 'true'); 41 | sweetAlert({ 42 | title: 'PCS Alert', 43 | html: true, 44 | confirmButtonText: "Close", 45 | text: 'Love PCS? Hate it? Want to vent about your life?
Take a quick survey!', 46 | type: 'warning' 47 | }); 48 | } 49 | } else { 50 | localStorage.setItem('secondvisit', 'true'); 51 | } 52 | } 53 | }); 54 | } 55 | var today = new Date(); 56 | if (today.getMonth() === 3 && today.getDate() === 1) { 57 | $('.fa-volume-off').css("visibility", "visible"); 58 | $('body').append(''); 59 | } 60 | // GA Tracking 61 | (function (i, s, o, g, r, a, m) { 62 | i['GoogleAnalyticsObject'] = r;i[r] = i[r] || function () { 63 | (i[r].q = i[r].q || []).push(arguments); 64 | }, i[r].l = 1 * new Date();a = s.createElement(o), m = s.getElementsByTagName(o)[0];a.async = 1;a.src = g;m.parentNode.insertBefore(a, m); 65 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); 66 | 67 | ga('create', 'UA-49014722-4', 'auto'); 68 | ga('send', 'pageview'); 69 | 70 | if (!window.File) { 71 | $('#ImportButton').hide(); 72 | } 73 | }); 74 | 75 | function ErrorAlert(err) { 76 | // Shows an error dialog and logs the error to the console 77 | // Also includes the error report in an email that can be sent to Ben 78 | var errtext = 'An error occurred. Refresh or email Ben'; 79 | 80 | if (err.status === 512) { 81 | errtext = "PennInTouch just crapped out on us. Please try again."; 82 | } else if (err.status === 513) { 83 | errtext = "PennInTouch is refreshing, so we can't access class info :(
Please frustratedly wait about half an hour before trying again."; 84 | } 85 | sweetAlert({ 86 | title: '#awkward', 87 | html: true, 88 | text: errtext, 89 | type: 'error' 90 | }); 91 | } 92 | 93 | function Uniquify(str, arr) { 94 | // Given an array and a string, this ensures that the string doesn't already exist in the array 95 | if (arr.indexOf(str) === -1) { 96 | return str; 97 | } else { 98 | // If it does, then we "+1" the string 99 | var lastchar = str[str.length - 1]; 100 | if (isNaN(lastchar) || str[str.length - 2] !== ' ') { 101 | // e.g. if string == 'schedule' or 'ABC123' 102 | str += ' 2'; // becomes 'schedule 2' or 'ABC123 2' 103 | } else { 104 | // e.g. 'MEAM 101 2' 105 | str = str.slice(0, -2) + ' ' + (parseInt(lastchar) + 1); // becomes "MEAM 101 3" 106 | } 107 | return Uniquify(str, arr); // Make sure that this new name is unique 108 | } 109 | } 110 | 111 | var delay = function () { 112 | var timer = 0; 113 | return function (callback, ms) { 114 | clearTimeout(timer); 115 | timer = setTimeout(callback, ms); 116 | }; 117 | }(); 118 | 119 | shuffle = function shuffle(v) { 120 | // Randomly reorders an array. 121 | //+ Jonas Raoni Soares Silva @ http://jsfromhell.com/array/shuffle [v1.0] 122 | for (var j, x, i = v.length; i; j = parseInt(Math.random() * i), x = v[--i], v[i] = v[j], v[j] = x) {} 123 | return v; 124 | }; 125 | 126 | function addrem(item, array) { 127 | // Adds or removes an item from an array depending on whether the array already contains that item. 128 | var index = array.indexOf(item); 129 | if (index === -1) { 130 | array.push(item); 131 | } else { 132 | array.splice(index, 1); 133 | } 134 | return array; 135 | } 136 | 137 | function FormatID(rawParam) { 138 | var searchParam = rawParam.replace(/\W/g, ''); // Replace non alpha-numeric characters 139 | var retArr = ['', '', '']; 140 | 141 | if (isFinite(searchParam[2])) { 142 | // If the third character is a number (e.g. BE100) 143 | splitTerms(2); 144 | } else if (isFinite(searchParam[3])) { 145 | // If the fourth character is a number (e.g. CIS110) 146 | splitTerms(3); 147 | } else if (isFinite(searchParam[4])) { 148 | // If the fifth character is a number (e.g. MEAM110) 149 | splitTerms(4); 150 | } else { 151 | retArr[0] = searchParam; 152 | } 153 | 154 | function splitTerms(n) { 155 | retArr[0] = searchParam.substr(0, n); 156 | retArr[1] = searchParam.substr(n, 3); 157 | retArr[2] = searchParam.substr(n + 3, 3); 158 | } 159 | 160 | return retArr; 161 | } 162 | 163 | function Schedule(term) { 164 | // This is a blank schedule object constructor 165 | this.term = term; // e.g. "2016A" 166 | this.meetings = []; 167 | this.colorPalette = ["#e74c3c", "#f1c40f", "#3498db", "#9b59b6", "#e67e22", "#2ecc71", "#95a5a6", "#FF73FD", "#73F1FF", "#CA75FF", "#1abc9c", "#F64747", "#ecf0f1"]; // Standard colorPalette 168 | this.locAdded = false; 169 | } 170 | 171 | function GenMeetBlocks(sec) { 172 | var blocks = []; 173 | for (var day in sec.meetDay) { 174 | if (sec.meetDay.hasOwnProperty(day)) { 175 | var meetLetterDay = sec.meetDay[day]; // On which day does this meeting take place? 176 | var meetRoom = sec.meetLoc; 177 | var newid = sec.idDashed + '-' + meetLetterDay + sec.meetHour.toString().replace(".", ""); 178 | var asscsecs = sec.SchedAsscSecs; 179 | 180 | var newblock = { 181 | 'class': sec.idDashed, 182 | 'letterday': meetLetterDay, 183 | 'id': newid, 184 | 'startHr': sec.meetHour, 185 | 'duration': sec.hourLength, 186 | 'name': sec.idSpaced, 187 | 'room': meetRoom, 188 | 'asscsecs': asscsecs, 189 | "topc": "blue" 190 | }; 191 | blocks.push(newblock); 192 | } 193 | } 194 | return blocks; 195 | } 196 | 197 | function TwoOverlap(block1, block2) { 198 | // Thank you to Stack Overflow user BC. for the function this is based on. 199 | // http://stackoverflow.com/questions/5419134/how-to-detect-if-two-divs-touch-with-jquery 200 | var y1 = block1.startHr || block1.top; 201 | var h1 = block1.duration || block1.height; 202 | var b1 = y1 + h1; 203 | 204 | var y2 = block2.startHr || block2.top; 205 | var h2 = block2.duration || block2.height; 206 | var b2 = y2 + h2; 207 | 208 | // This checks if the top of block 2 is lower down (higher value) than the bottom of block 1... 209 | // or if the top of block 1 is lower down (higher value) than the bottom of block 2. 210 | // In this case, they are not overlapping, so return false 211 | if (b1 <= y2 + 0.0000001 || b2 <= y1 + 0.0000001) { 212 | return false; 213 | } else { 214 | return true; 215 | } 216 | } -------------------------------------------------------------------------------- /importSched.js: -------------------------------------------------------------------------------- 1 | function readCalFile() { 2 | var files = $('#schedInput')[0].files; 3 | if (!files || !files.length) { 4 | return; 5 | } 6 | 7 | var file = files[0]; 8 | var reader = new FileReader(); 9 | // If we use onloadend, we need to check the readyState. 10 | reader.onloadend = function (evt) { 11 | if (evt.target.readyState === FileReader.DONE) { 12 | // DONE == 2 13 | var secArr = parseCalFile(evt.target.result); 14 | if (secArr.length) { 15 | $('#importSubmit').prop('disabled', false); 16 | } else { 17 | $('#importSubmit').prop('disabled', true); 18 | } 19 | $('#secsToImport').empty(); 20 | for (var i = 0; i < secArr.length; i++) { 21 | $('#secsToImport').append('' + FormatID(secArr[i]).join(' ') + '
'); 22 | } 23 | return secArr; 24 | } else { 25 | $('#importSubmit').prop('disabled', true); 26 | return []; 27 | } 28 | }; 29 | var blob = file.slice(0, file.size - 1); 30 | reader.readAsBinaryString(blob); 31 | } 32 | 33 | function parseCalFile(rawCal) { 34 | function FilterFunc(line) { 35 | if (line.split(':')[0] === 'SUMMARY') { 36 | return 1; 37 | } 38 | } 39 | function MapFunc(line) { 40 | return line.split('\r')[0].replace(/ /g, '').split(':')[1]; 41 | } 42 | 43 | var secs = rawCal.split('\n').filter(FilterFunc).map(MapFunc); 44 | var uniq = secs.filter(function (elem, pos) { 45 | return secs.indexOf(elem) === pos; 46 | }); 47 | return uniq; 48 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | console.time('Modules loaded'); 2 | // Initial configuration 3 | var path = require('path'); 4 | var express = require('express'); 5 | var compression = require('compression'); 6 | var request = require('request'); 7 | var Keen = require('keen-js'); 8 | var git = require('git-rev'); 9 | var helmet = require('helmet'); 10 | require('log-timestamp')(function() { return new Date().toISOString() + ' %s'; }); 11 | 12 | console.timeEnd('Modules loaded'); 13 | 14 | // I don't want to host a config file on Github. When running locally, the app has access to a local config file. 15 | // In production, there is no config file so I use environment variables instead 16 | var config; 17 | try { 18 | config = require('./config.js'); 19 | } catch (err) { // If there is no config file 20 | config = {}; 21 | config.requestAB = process.env.REQUESTAB; 22 | config.requestAT = process.env.REQUESTAT; 23 | config.PCRToken = process.env.PCRTOKEN; 24 | config.KeenIOID = process.env.KEEN_PROJECT_ID; 25 | config.KeenIOWriteKey = process.env.KEEN_WRITE_KEY; 26 | config.autotestKey = process.env.AUTOTESTKEY; 27 | } 28 | 29 | // Set express settings 30 | var app = express(); 31 | app.use(compression()); 32 | app.use(helmet()); // Hides certain HTTP headers, supposedly more secure? 33 | 34 | // I want this to have the client always pull the newest JS, since uodates happen very often. 35 | // IDK if this is the best way to do that. 36 | app.use('/js/plugins', express.static(path.join(__dirname, 'public/js/plugins'), { maxAge: 2628000000 })); 37 | app.use('/js', express.static(path.join(__dirname, 'public/js'), { maxAge: 0 })); 38 | app.use(express.static(path.join(__dirname, 'public'), { maxAge: 2628000000 })); 39 | 40 | console.log('Express initialized'); 41 | 42 | // Set up Keen Analytics 43 | var client; 44 | var keenEnable = true; 45 | if (process.env.NODE_ENV === 'production' && keenEnable) { // Only log from production 46 | console.log('KeenIO logging enabled'); 47 | client = new Keen({ 48 | projectId: config.KeenIOID, // String (required always) 49 | writeKey: config.KeenIOWriteKey // String (required for sending data) 50 | }); 51 | } 52 | function logEvent(eventName, eventData) { 53 | if (client) { 54 | client.addEvent(eventName, eventData, function (err) { 55 | if (err) { 56 | console.log("KEENIOERROR: " + err); 57 | } 58 | }); 59 | } 60 | } 61 | 62 | console.log('Plugins initialized'); 63 | 64 | git.short(function (str) { 65 | console.log('Current git commit:', str); // log the current commit we are running 66 | }); 67 | 68 | var currentTerm = '2019A'; // Which term is currently active 69 | var LRTimes = Array(config.requestAB.length).fill(0); // Timestamps of latest requests using each OpenData key (see OpenData.js) 70 | var ODkeyInd = 0; // Which key to use next 71 | 72 | // Pull in external data and functions 73 | var allCourses = require('./loadCourses.js')(currentTerm); // Get array of all courses 74 | var parse = require('./parse.js'); // Load the parsing functions 75 | var opendata = require('./opendata.js')(95); 76 | 77 | var listenPort = 3000; 78 | if (process.argv[1].includes('beta')) { // If running in the staging environment, run on a different port 79 | listenPort = 3001; 80 | } 81 | 82 | // Start the server 83 | app.listen(listenPort, function(){ 84 | console.log("Node app is running on port "+listenPort+". Better go catch it."); 85 | }); 86 | 87 | // Handle main page requests 88 | app.get('/', function(req, res) { 89 | res.sendFile(path.join(__dirname+'/public/index.html')); 90 | }); 91 | 92 | // Handle status requests. This lets the admin disseminate info if necessary 93 | app.get('/Status', function(req, res) { 94 | var statustext = 'hakol beseder'; // Means "everything is ok" in Hebrew 95 | // statustext = 'Penn InTouch is being MERT\'ed right now, so PennCourseSearch may not work correctly. Please try again later if you run into issues.' 96 | 97 | // Penn InTouch often is refreshing data between 1:00 AM and 5:00 AM, which renders the API useless. 98 | // This is just letting the user know. 99 | var now = new Date(); 100 | var hour = now.getHours(); 101 | if (hour >= 1 && hour < 5) { 102 | statustext = "Penn InTouch sometimes screws up around this time of night, which can cause problems with PennCourseSearch.
Sorry in advance."; 103 | } 104 | return res.send(statustext); 105 | }); 106 | 107 | var searchTypes = { 108 | courseIDSearch: '&course_id=', 109 | keywordSearch: '&description=', 110 | instSearch: '&instructor=' 111 | }; 112 | 113 | var filterURI = { 114 | reqFilter: '&fulfills_requirement=', 115 | proFilter: '&program=', 116 | actFilter: '&activity=', 117 | includeOpen: '&open=true' 118 | }; 119 | 120 | var buildURI = function (filter, type) { // Build the request URI given certain filters and requirements 121 | if (typeof filter === 'undefined') { 122 | return ''; 123 | } else { 124 | if (type === 'includeOpen') { 125 | return filterURI[type]; 126 | } else { 127 | return filterURI[type] + filter; 128 | } 129 | } 130 | }; 131 | 132 | var BASE_URL = 'https://esb.isc-seo.upenn.edu/8091/open_data/course_section_search?number_of_results_per_page=500&term='; 133 | 134 | // Manage search requests 135 | app.get('/Search', function(req, res) { 136 | var searchParam = req.query.searchParam; // The search terms 137 | var searchType = req.query.searchType; // Course ID, Keyword, or Instructor 138 | var resultType = req.query.resultType; // Course numbers, section numbers, section info 139 | var instructFilter = req.query.instFilter; // Is there an instructor filter? 140 | 141 | // Keen.io logging 142 | if (searchParam) { 143 | var searchEvent = {searchParam: searchParam}; 144 | logEvent('Search', searchEvent); 145 | } 146 | 147 | if (searchType === 'courseIDSearch' && resultType === 'deptSearch' && !req.query.proParam) { // If we can return results from cached data 148 | var returnCourses = allCourses; 149 | 150 | if (searchParam) { // Filter by department 151 | returnCourses = returnCourses.filter(function(obj) {return (obj.idDashed.split('-')[0] === searchParam.toUpperCase());}); 152 | } 153 | if (req.query.reqParam) { // Filter by requirement 154 | returnCourses = returnCourses.filter(function(obj) {return ((obj.courseReqs.indexOf(req.query.reqParam) > -1));}); 155 | } 156 | 157 | retC = parse.DeptList(returnCourses); 158 | return res.send(returnCourses); 159 | 160 | } else { // Otherwise, ask the API 161 | // Building the request URI 162 | var reqSearch = buildURI("", 'reqFilter'); 163 | // Don't try to ask API about Wharton and Engineering requirements 164 | if (!(req.query.reqParam && (req.query.reqParam.charAt(0) === "W" || req.query.reqParam.charAt(0) === "E"))) { 165 | 166 | // For some reason, these two req codes need extra characters at the end when searching the API 167 | if (req.query.reqParam === 'MDO' || req.query.reqParam === 'MDN') { 168 | req.query.reqParam += ',MDB'; 169 | } 170 | reqSearch = buildURI(req.query.reqParam, 'reqFilter'); 171 | } 172 | 173 | var proSearch = buildURI(req.query.proParam, 'proFilter'); 174 | var actSearch = buildURI(req.query.actParam, 'actFilter'); 175 | var includeOpen = buildURI(req.query.openAllow, 'includeOpen'); 176 | 177 | var baseURL = BASE_URL + currentTerm + reqSearch + proSearch + actSearch + includeOpen; 178 | if (searchType) { 179 | baseURL += searchTypes[searchType] + decodeURIComponent(searchParam); 180 | } 181 | // If we are searching by a certain instructor, the course numbers will be filtered because of searchType 'instSearch'. 182 | // However, clicking on one of those courses will show all sections, including those not taught by the instructor. 183 | // instructFilter is an extra parameter that allows further filtering of section results by instructor. 184 | if (instructFilter !== 'all' && typeof instructFilter !== 'undefined') { 185 | baseURL += '&instructor=' + instructFilter; 186 | } 187 | 188 | // Send request to OpenData and record the timestamp of the request. Arguments: 189 | // requestURL 190 | // parse function to send result 191 | // res function to send response to client 192 | // Latest timestamp 193 | // Auth key to use 194 | LRTimes[ODkeyInd] = opendata.RateLimitReq(baseURL, resultType, res, LRTimes[ODkeyInd], ODkeyInd); 195 | ODkeyInd++; // Use the other auth key next time 196 | if (ODkeyInd >= LRTimes.length) { 197 | ODkeyInd = 0; 198 | } 199 | } 200 | }); 201 | 202 | // Manage scheduling requests 203 | app.get('/Sched', function(req, res) { 204 | var courseID = req.query.courseID; 205 | var needLoc = req.query.needLoc; 206 | var uri = 'https://esb.isc-seo.upenn.edu/8091/open_data/course_section_search?term='+currentTerm+'&course_id='+courseID; 207 | var schedEvent = {schedCourse: courseID}; 208 | if (!needLoc) {logEvent('Sched', schedEvent);} 209 | LRTimes[ODkeyInd] = opendata.RateLimitReq(uri, 'schedInfo', res, LRTimes[ODkeyInd], ODkeyInd); 210 | ODkeyInd = 1 - ODkeyInd; 211 | }); 212 | 213 | // // Handle requests with PennCourseAlert 214 | // app.post('/Alert', function(req, res) { 215 | // var secID = req.query.secID; 216 | // // var formatSecID = secID.replace(/-/g, ' '); 217 | // var userEmail = req.query.email; 218 | // var userPhone = req.query.phone; 219 | // var userCarrier = req.query.carrier; 220 | 221 | // var formdata = {'course': secID}; 222 | // if (userEmail) {formdata.email = userEmail;} 223 | // if (userPhone && userCarrier) { 224 | // formdata.phone = userPhone; 225 | // formdata.carrier = userCarrier; 226 | // } 227 | 228 | // logEvent('Alert', {alertsec: secID}); 229 | // console.log(JSON.stringify(formdata)) 230 | // request({ 231 | // uri: 'http://www.penncoursealert.com/submitted/', 232 | // method: "POST", 233 | // form: formdata 234 | // }, function(error, response, body) { 235 | // console.log(JSON.stringify(response)) 236 | // var returnText = "Sorry, there was an error while trying set up notifications."; 237 | // // res.statusCode = 201; 238 | // // if (error) { 239 | // // console.log('PCN req error:', error); 240 | // // } else { 241 | // // try { 242 | // // if (response.statusCode === 406) { 243 | // // returnText = "Notifications already requested."; 244 | // // res.statusCode = 200; 245 | // // } else if (body.split('

')[1].split('

')[0] === "Success!") { 246 | // // returnText = "Great! You'll be notified if "+secID+" opens up."; 247 | // // res.statusCode = 200; 248 | // // } 249 | // // } catch(err) { 250 | // // console.log('Notify Error:', err); 251 | // // } 252 | // // } 253 | // return res.send(returnText); 254 | // }); 255 | // }); -------------------------------------------------------------------------------- /like_button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 8 | 9 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 10 | 11 | var LikeButton = function (_React$Component) { 12 | _inherits(LikeButton, _React$Component); 13 | 14 | function LikeButton(props) { 15 | _classCallCheck(this, LikeButton); 16 | 17 | var _this = _possibleConstructorReturn(this, (LikeButton.__proto__ || Object.getPrototypeOf(LikeButton)).call(this, props)); 18 | 19 | _this.state = { liked: false }; 20 | return _this; 21 | } 22 | 23 | _createClass(LikeButton, [{ 24 | key: 'render', 25 | value: function render() { 26 | var _this2 = this; 27 | 28 | if (this.state.liked) { 29 | return 'You liked this.'; 30 | } 31 | 32 | return React.createElement( 33 | 'button', 34 | { onClick: function onClick() { 35 | return _this2.setState({ liked: true }); 36 | } }, 37 | 'Like' 38 | ); 39 | } 40 | }]); 41 | 42 | return LikeButton; 43 | }(React.Component); 44 | 45 | var domContainer = document.querySelector('#react_root'); 46 | ReactDOM.render(React.createElement(LikeButton, null), domContainer); -------------------------------------------------------------------------------- /loadCourses.js: -------------------------------------------------------------------------------- 1 | module.exports = function(CourseTerm) { 2 | var deptList = ["AAMW", "ACCT", "AFRC", "AFST", "ALAN", "AMCS", "ANCH", "ANEL", "ANTH", "ARAB", "ARCH", "ARTH", "ASAM", "ASTR", "BCHE", "BDS", "BE", "BENF", "BENG", "BEPP", "BIBB", "BIOE", "BIOL", "BIOM", "BIOT", "BMB", "BMIN", "BSTA", "CAMB", "CBE", "CHEM", "CHIN", "CIMS", "CIS", "CIT", "CLST", "COGS", "COML", "COMM", "CPLN", "CRIM", "DEMG", "DENT", "DPED", "DPRD", "DRST", "DTCH", "DYNM", "EALC", "EAS", "ECON", "EDUC", "EEUR", "ENGL", "ENGR", "ENM", "ENMG", "ENVS", "EPID", "ESE", "FNAR", "FNCE", "FOLK", "FREN", "GAFL", "GAS", "GCB", "GEOL", "GREK", "GRMN", "GSWS", "GUJR", "HCIN", "HCMG", "HEBR", "HIND", "HIST", "HPR", "HSOC", "HSPV", "HSSC", "IMUN", "INTG", "INTL", "INTR", "INTS", "IPD", "ITAL", "JPAN", "JWST", "KORN", "LALS", "LARP", "LATN", "LAW", "LAWM", "LGIC", "LGST", "LING", "LSMP", "MATH", "MCS", "MEAM", "MED", "MGEC", "MGMT", "MKTG", "MLA", "MLYM", "MMP", "MSCI", "MSE", "MSSP", "MTR", "MUSA", "MUSC", "NANO", "NELC", "NETS", "NGG", "NPLD", "NSCI", "NURS", "OIDD", "PERS", "PHIL", "PHRM", "PHYS", "PPE", "PREC", "PRTG", "PSCI", "PSYC", "PUBH", "PUNJ", "REAL", "REG", "RELS", "ROML", "RUSS", "SAST", "SCND", "SKRT", "SLAV", "SOCI", "SPAN", "STAT", "STSC", "SWRK", "TAML", "TELU", "THAR", "TURK", "URBS", "URDU", "VBMS", "VCSN", "VCSP", "VIPR", "VISR", "VLST", "VMED", "VPTH", "WH", "WHCP", "WHG", "WRIT", "YDSH"]; 3 | 4 | var allCourses = []; 5 | for (var dept in deptList) { if (deptList.hasOwnProperty(dept)) { 6 | try { 7 | var thedept = deptList[dept]; 8 | var newte = require('./Data/'+CourseTerm+'/'+thedept+'.json'); 9 | allCourses = allCourses.concat(newte); 10 | } catch(err) { 11 | 12 | } 13 | }} 14 | return allCourses; 15 | }; -------------------------------------------------------------------------------- /loadRevs.js: -------------------------------------------------------------------------------- 1 | var deptList = ["AAMW", "ACCT", "AFRC", "AFST", "ALAN", "AMCS", "ANCH", "ANEL", "ANTH", "ARAB", "ARCH", "ARTH", "ASAM", "ASTR", "BCHE", "BDS", "BE", "BENF", "BENG", "BEPP", "BIBB", "BIOE", "BIOL", "BIOM", "BIOT", "BMB", "BMIN", "BSTA", "CAMB", "CBE", "CHEM", "CHIN", "CIMS", "CIS", "CIT", "CLST", "COGS", "COML", "COMM", "CPLN", "CRIM", "DEMG", "DENT", "DPED", "DPRD", "DRST", "DTCH", "DYNM", "EALC", "EAS", "ECON", "EDUC", "EEUR", "ENGL", "ENGR", "ENM", "ENMG", "ENVS", "EPID", "ESE", "FNAR", "FNCE", "FOLK", "FREN", "GAFL", "GAS", "GCB", "GEOL", "GREK", "GRMN", "GSWS", "GUJR", "HCIN", "HCMG", "HEBR", "HIND", "HIST", "HPR", "HSOC", "HSPV", "HSSC", "IMUN", "INTG", "INTL", "INTR", "INTS", "IPD", "ITAL", "JPAN", "JWST", "KORN", "LALS", "LARP", "LATN", "LAW", "LAWM", "LGIC", "LGST", "LING", "LSMP", "MATH", "MCS", "MEAM", "MED", "MGEC", "MGMT", "MKTG", "MLA", "MLYM", "MMP", "MSCI", "MSE", "MSSP", "MTR", "MUSA", "MUSC", "NANO", "NELC", "NETS", "NGG", "NPLD", "NSCI", "NURS", "OIDD", "PERS", "PHIL", "PHRM", "PHYS", "PPE", "PREC", "PRTG", "PSCI", "PSYC", "PUBH", "PUNJ", "REAL", "REG", "RELS", "ROML", "RUSS", "SAST", "SCND", "SKRT", "SLAV", "SOCI", "SPAN", "STAT", "STSC", "SWRK", "TAML", "TELU", "THAR", "TURK", "URBS", "URDU", "VBMS", "VCSN", "VCSP", "VIPR", "VISR", "VLST", "VMED", "VPTH", "WH", "WHCP", "WHG", "WRIT", "YDSH"]; 2 | 3 | var allRevs = {}; 4 | for (var dept in deptList) { if (deptList.hasOwnProperty(dept)) { 5 | try { 6 | var thedept = deptList[dept]; 7 | allRevs[thedept] = require('./Data/2017CRev/'+thedept); 8 | } catch(err) { 9 | 10 | } 11 | }} 12 | module.exports = allRevs; -------------------------------------------------------------------------------- /opendata.js: -------------------------------------------------------------------------------- 1 | /* 2 | This module has functions used to interface with the Penn OpenData API. 3 | It allows for rate-limiting requests and granular error reporting. 4 | The main function is RateLimitReq. 5 | */ 6 | 7 | module.exports = function(rpm) { 8 | var request = require('request'); 9 | var parse = require('./parse.js'); 10 | var config; 11 | try { 12 | config = require('./config.js'); 13 | } catch (err) { // If there is no config file 14 | config = {}; 15 | config.requestAB = process.env.REQUESTAB; 16 | config.requestAT = process.env.REQUESTAT; 17 | } 18 | var opendata = {}; 19 | 20 | if (config.IFTTTKey) { 21 | var IFTTTMaker = require('iftttmaker')(config.IFTTTKey); 22 | } 23 | function SendError(errmsg) { // Send an email to Ben through IFTTT when there is a server error 24 | if (config.IFTTTKey) { 25 | IFTTTMaker.send('PCSError', errmsg).then(function () { 26 | }).catch(function (error) { 27 | console.log('The error request could not be sent:', error); 28 | }); 29 | } 30 | } 31 | 32 | // Send requests to the API and deal with the result 33 | function SendPennReq(url, resultType, res, ODkeyInd) { 34 | // Choose the auth bearer and token pair to use based on ODkeyInd 35 | var AB = config.requestAB[ODkeyInd]; 36 | var AT = config.requestAT[ODkeyInd]; 37 | // Send the request 38 | request({ 39 | uri: url, 40 | method: "GET",headers: {"Authorization-Bearer": AB, "Authorization-Token": AT}, // Send authorization headers 41 | timeout: 19000, 42 | }, function(error, response, body) { 43 | 44 | if (error || response.statusCode >= 500) { 45 | // This is triggered if the OpenData API returns an error or never responds 46 | console.log(JSON.stringify(response)); 47 | console.log('OpenData Request failed:', url, error); 48 | SendError('OpenData Request failed'); 49 | res.statusCode = 512; // Reserved error code to tell front end that its a Penn InTouch problem, not a PCS problem 50 | return res.send('PCSERROR: request failed'); 51 | } 52 | 53 | var parsedRes, rawResp = {}; 54 | try { // Try to make body into valid JSON 55 | rawResp = JSON.parse(body); 56 | if (rawResp.statusCode) { 57 | SendError('Status Code'); 58 | res.statusCode = 512; // Reserved error code to tell front end that its a Penn InTouch problem, not a PCS problem 59 | return res.send('status code error'); 60 | } 61 | } catch(err) { // Could not parse the JSON for some reason 62 | console.log('Resp parse error ' + err); 63 | SendError('Parse Error!!!'); 64 | console.log(JSON.stringify(response)); 65 | return res.send({}); 66 | } 67 | 68 | try { 69 | if (rawResp.service_meta.error_text) { // If the API returned an error in its response 70 | console.log('Resp Err: ' + rawResp.service_meta.error_text); 71 | SendError('Error Text'); 72 | res.statusCode = 513; // Reserved error code to tell front end that its a Penn InTouch problem, not a PCS problem 73 | return res.send(rawResp.service_meta.error_text); 74 | } 75 | parsedRes = rawResp.result_data; 76 | } catch(err) { 77 | console.log(err); 78 | SendError('Other Error!!!'); 79 | res.statusCode = 500; 80 | return res.send(err); 81 | } 82 | 83 | // Route the parsed response to the correct function 84 | RouteAPIResp(resultType, parsedRes, res); 85 | }); 86 | } 87 | 88 | function RouteAPIResp(resultType, parsedRes, res) { // Send the raw data to the appropriate formatting function 89 | var resultTypes = { 90 | deptSearch: parse.CourseList, 91 | numbSearch: parse.SectionList, 92 | sectSearch: parse.SectionInfo, 93 | schedInfo: parse.SchedInfo, 94 | registrar: parse.RecordRegistrar 95 | }; 96 | 97 | var searchResponse; 98 | if (resultType in resultTypes) { 99 | searchResponse = resultTypes[resultType](parsedRes); 100 | } else { 101 | searchResponse = {}; 102 | } 103 | if (res.send) {return res.send(JSON.stringify(searchResponse));} // return correct info 104 | } 105 | 106 | // Delay a requests the necessary amount of time before sending it to the API 107 | if (!rpm) {rpm = 95;} 108 | 109 | opendata.RateLimitReq = function(url, resultType, res, lastRT, ODkeyInd) { 110 | // lastRT is the timestamp of the last sent request using this auth key 111 | 112 | var period = 60000 / rpm; 113 | var now = new Date().getTime(); 114 | var diff = now - lastRT; // how long ago was the last request point 115 | var delay = (period - diff) * (diff < period); // How long to delay the request (if diff is > period, no need to delay) 116 | var lastRequestTime = now+delay; // Update the latest request timestamp (will be a future timestamp if a delay is added) 117 | 118 | setTimeout(function() { 119 | SendPennReq(url, resultType, res, ODkeyInd) ;// Send the request after the delay 120 | }, delay); 121 | return lastRequestTime; // Return the latest request time for recording. 122 | }; 123 | 124 | return opendata; 125 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PennCourseSearch", 3 | "version": "1.0.0", 4 | "description": "A barebones web app designed to help Penn students find classes and make schedules", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "colors": "^1.1.2", 11 | "compression": "^1.7.0", 12 | "express": "^4.15.3", 13 | "fs": "0.0.2", 14 | "git-rev": "^0.2.1", 15 | "helmet": "^3.11.0", 16 | "iftttmaker": "^1.2.1", 17 | "keen-js": "^3.4.1", 18 | "log-timestamp": "^0.1.2", 19 | "nodemon": "^1.17.5", 20 | "npm": "^5.6.0", 21 | "path": "^0.12.7", 22 | "request": "^2.81.0" 23 | }, 24 | "engines": { 25 | "node": "5.4.x" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/benb116/PennCourseSearch" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-preset-react-app": "^3.1.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | var parse = {}; 2 | var r = require('./reqFunctions.js'); // Functions that help determine which req rules apply to a class 3 | var allRevs = require('./loadRevs.js'); 4 | 5 | function GetRevData (dept, num, inst) { 6 | // Given a department, course number, and instructor (optional), get back the three rating values 7 | var deptData = allRevs[dept]; // Get dept level ratings 8 | var thisRevData = {"cQ": 0, "cD": 0, "cI": 0}; 9 | if (deptData) { 10 | var revData = deptData[num]; // Get course level ratings 11 | if (revData) { 12 | // Try to get instructor specific reviews, but fallback to general course reviews 13 | thisRevData = (revData[(inst || '').trim().toUpperCase()] || (revData.Recent)); 14 | } 15 | } 16 | return thisRevData; 17 | } 18 | 19 | function getTimeInfo(JSONObj) { // A function to retrieve and format meeting times 20 | var OCStatus = JSONObj.course_status; // Is the section open or closed 21 | var isOpen; 22 | if (OCStatus === "O") { 23 | isOpen = true; 24 | } else { 25 | isOpen = false; 26 | } 27 | var TimeInfo = []; // TimeInfo is textual e.g. '10:00 to 11:00 on MWF' 28 | try { // Not all sections have time info 29 | for(var meeting in JSONObj.meetings) { if (JSONObj.meetings.hasOwnProperty(meeting)) { 30 | // Some sections have multiple meeting forms (I'm looking at you PHYS151) 31 | var thisMeet = JSONObj.meetings[meeting]; 32 | var StartTime= thisMeet.start_time.split(" ")[0]; // Get start time 33 | var EndTime = thisMeet.end_time.split(" ")[0]; // Get end time 34 | 35 | if (StartTime[0] === '0') { 36 | StartTime = StartTime.slice(1); 37 | } // If it's 08:00, make it 8:00 38 | if (EndTime[0] === '0') { 39 | EndTime = EndTime.slice(1); 40 | } 41 | 42 | var MeetDays = thisMeet.meeting_days; // Output like MWF or TR 43 | var meetListInfo = StartTime+" to "+EndTime+" on "+MeetDays; 44 | TimeInfo.push(meetListInfo); 45 | }} 46 | } 47 | catch (err) { 48 | console.log(("Error getting times", JSONObj.section_id)); 49 | TimeInfo = ''; 50 | } 51 | return [isOpen, TimeInfo]; 52 | } 53 | 54 | function SectionMeeting(sec) { 55 | var isOpen = (sec.course_status !== 'X') && (Number(sec.course_number) < 600); 56 | var thisInfo = { 57 | 'idDashed': sec.course_department + '-' + sec.course_number + '-' + sec.section_number, 58 | 'course': sec.course_department + '-' + sec.course_number, 59 | 'actType': sec.activity, 60 | 'assclec': sec.lectures, 61 | 'assclab': sec.labs, 62 | 'asscrec': sec.recitations, 63 | 'meetblk': [], 64 | 'open': isOpen 65 | }; 66 | sec.meetings.forEach(function(meeting) { 67 | for (i = 0; i < meeting.meeting_days.length; i++) { 68 | thisInfo.meetblk.push({ 69 | 'meetday': meeting.meeting_days[i], 70 | 'starthr': meeting.start_hour_24 + (meeting.start_minutes)/60, 71 | 'endhr': meeting.end_hour_24 + (meeting.end_minutes)/60 72 | }); 73 | } 74 | }); 75 | return thisInfo; 76 | } 77 | 78 | parse.SchedInfo = function(entry) { // Get the properties required to schedule the section 79 | if (entry.result_data) { 80 | entry = entry.result_data; 81 | } else if (!entry[0]) { 82 | entry = [entry]; 83 | } 84 | var resJSON = []; 85 | for (var key in entry) { if (entry.hasOwnProperty(key)) { 86 | var idDashed = entry[key].section_id_normalized.replace(/ /g, ""); // Format ID 87 | var idSpaced = idDashed.replace(/-/g, ' '); 88 | try { // Not all sections have time info 89 | for(var meeti in entry[key].meetings) { if (entry[key].meetings.hasOwnProperty(meeti)) { // Some sections have multiple meetings 90 | var thisMeet = entry[key].meetings[meeti]; 91 | var StartTime = (thisMeet.start_hour_24) + (thisMeet.start_minutes)/60; 92 | var EndTime = (thisMeet.end_hour_24) + (thisMeet.end_minutes)/60; 93 | var hourLength = EndTime - StartTime; 94 | var MeetDays = thisMeet.meeting_days; 95 | var Building = (thisMeet.building_code || ''); 96 | var Room = (thisMeet.room_number || ''); 97 | var SchedAsscSecs = []; 98 | if (entry[key].lectures.length) { 99 | SchedAsscSecs = entry[key].lectures.map(function(a) { 100 | return [a.subject, a.course_id, a.section_id].join('-'); 101 | }); 102 | } else if (entry[key].recitations.length) { 103 | SchedAsscSecs = entry[key].recitations.map(function(a) { 104 | return [a.subject, a.course_id, a.section_id].join('-'); 105 | }); 106 | } else if (entry[key].labs.length) { 107 | SchedAsscSecs = entry[key].labs.map(function(a) { 108 | return [a.subject, a.course_id, a.section_id].join('-'); 109 | }); 110 | } 111 | 112 | // Full ID will have sectionID+MeetDays+StartTime 113 | // This is necessary for classes like PHYS151, which has times: M@13, TR@9, AND R@18 114 | var FullID = idDashed+'-'+MeetDays+StartTime.toString().replace(".", ""); 115 | 116 | resJSON.push({ 117 | 'fullID': FullID, 118 | 'idDashed': idDashed, 119 | 'idSpaced': idSpaced, 120 | 'hourLength': hourLength, 121 | 'meetDay': MeetDays, 122 | 'meetHour': StartTime, 123 | 'meetLoc': Building+' '+Room, 124 | 'SchedAsscSecs': SchedAsscSecs 125 | }); 126 | }} 127 | } 128 | catch (err) { 129 | console.log("Error getting times: "+err); 130 | } 131 | }} 132 | return resJSON; 133 | }; 134 | 135 | parse.DeptList = function(res) { 136 | for (var course in res) { if (res.hasOwnProperty(course)) { 137 | var courData = res[course].idSpaced.split(' '); 138 | var courDept = courData[0]; 139 | var courNum = courData[1]; 140 | res[course].revs = GetRevData(courDept, courNum); // Append PCR data to courses 141 | }} 142 | return res; 143 | }; 144 | 145 | // This function spits out the array of courses that goes in #CourseList 146 | // Takes in data from the API 147 | parse.CourseList = function(Res) { 148 | var coursesList = {}; 149 | for(var key in Res) { if (Res.hasOwnProperty(key)) { 150 | var thisKey = Res[key]; 151 | 152 | if (!thisKey.is_cancelled) { // Iterate through each course that isn't cancelled 153 | var thisDept = thisKey.course_department.toUpperCase(); 154 | var thisNum = thisKey.course_number.toString(); 155 | var courseListName = thisDept+' '+thisNum; // Get course dept and number 156 | var numCred = Number(thisKey.credits.split(" ")[0]); // How many credits does this section count for 157 | 158 | if (!coursesList[courseListName]) { // If there's no entry, make a new one 159 | var courseTitle = thisKey.course_title; 160 | var reqCodesList = r.GetRequirements(thisKey); // Check which requirements are fulfilled by this course 161 | var revData = GetRevData(thisDept, thisNum); // Get review information 162 | coursesList[courseListName] = { 163 | 'idSpaced': courseListName, 164 | 'idDashed': courseListName.replace(/ /g,'-'), 165 | 'courseTitle': courseTitle, 166 | 'courseReqs': reqCodesList[0], 167 | 'courseCred': numCred, 168 | 'revs': revData 169 | }; 170 | } else if (coursesList[courseListName].courseCred < numCred) { // If there is an entry, choose the higher of the two numcred values 171 | coursesList[courseListName].courseCred = numCred; 172 | } 173 | } 174 | }} 175 | var arrResp = []; 176 | for (var course in coursesList) { if (coursesList.hasOwnProperty(course)) { 177 | arrResp.push(coursesList[course]); // Convert from object to array 178 | }} 179 | return arrResp; 180 | }; 181 | 182 | // This function spits out section-specific info 183 | parse.SectionInfo = function(Res) { 184 | var entry = Res[0]; 185 | var sectionInfo = {}; 186 | // try { 187 | if (entry && !entry.is_cancelled) { // Don't return cancelled sections 188 | var Title = entry.course_title; 189 | var FullID = entry.section_id_normalized.replace(/-/g, " "); // Format name 190 | var CourseID = entry.section_id_normalized.split('-')[0] + ' ' + entry.section_id_normalized.split('-')[1]; 191 | var Instructor = ((entry.instructors[0] && entry.instructors[0].name) || ''); 192 | var Desc = entry.course_description; 193 | var TimeInfoArray = getTimeInfo(entry); 194 | var StatusClass = TimeInfoArray[0]; 195 | var meetArray = TimeInfoArray[1]; 196 | var prereq = (entry.prerequisite_notes[0] || 'none'); 197 | var termsOffered = entry.course_terms_offered; 198 | 199 | var OpenClose = 'Closed'; 200 | if (StatusClass) { 201 | OpenClose = 'Open'; 202 | } 203 | var secCred = Number(entry.credits.split(" ")[0]); 204 | 205 | var asscType = ''; 206 | var asscList = []; 207 | if (entry.lectures.length) { 208 | asscType = 'lecture'; 209 | asscList = entry.lectures.map(function(a) { 210 | return [a.subject, a.course_id, a.section_id].join(' '); 211 | }); 212 | } else if (entry.recitations.length) { 213 | asscType = 'recitation'; 214 | asscList = entry.recitations.map(function(a) { 215 | return [a.subject, a.course_id, a.section_id].join(' '); 216 | }); 217 | } else if (entry.labs.length) { 218 | asscType = 'lab'; 219 | asscList = entry.labs.map(function(a) { 220 | return [a.subject, a.course_id, a.section_id].join(' '); 221 | }); 222 | } 223 | 224 | var reqsArray = r.GetRequirements(entry)[1]; 225 | 226 | sectionInfo = { 227 | 'fullID': FullID, 228 | 'CourseID': CourseID, 229 | 'title': Title, 230 | 'instructor': Instructor, 231 | 'description': Desc, 232 | 'openClose': OpenClose, 233 | 'termsOffered': termsOffered, 234 | 'prereqs': prereq, 235 | 'timeInfo': meetArray, 236 | 'associatedType': asscType, 237 | 'associatedSections': asscList, 238 | 'sectionCred': secCred, 239 | 'reqsFilled': reqsArray 240 | }; 241 | return sectionInfo; 242 | } else { 243 | return {}; 244 | } 245 | }; 246 | 247 | // This function spits out the list of sections that goes in #SectionList 248 | parse.SectionList = function(Res) { 249 | // Convert to JSON object 250 | var sectionsList = []; 251 | // var courseInfo = {}; 252 | for(var key in Res) { 253 | if (Res.hasOwnProperty(key)) { 254 | var thisEntry = Res[key]; 255 | if (!thisEntry.is_cancelled) { 256 | var idDashed = thisEntry.section_id_normalized.replace(/ /g, ""); 257 | var idSpaced = idDashed.replace(/-/g, ' '); 258 | var timeInfoArray = getTimeInfo(thisEntry); // Get meeting times for a section 259 | var isOpen = timeInfoArray[0]; 260 | var timeInfo = (timeInfoArray[1][0] || ''); // Get the first meeting slot 261 | if (timeInfoArray[1][1]) { // Cut off extra text 262 | timeInfo += ' ...'; 263 | } 264 | var actType = thisEntry.activity; 265 | var SectionInst = ((thisEntry.instructors[0] && thisEntry.instructors[0].name) || ''); // Get the instructor for this section 266 | var revData = GetRevData(thisEntry.course_department, thisEntry.course_number, SectionInst); // Get inst-specific reviews 267 | var schedInfo = parse.SchedInfo(thisEntry); 268 | 269 | sectionsList.push({ 270 | 'idDashed': idDashed, 271 | 'idSpaced': idSpaced, 272 | 'isOpen': isOpen, 273 | 'timeInfo': timeInfo, 274 | 'courseTitle': Res[0].course_title, 275 | 'SectionInst': SectionInst, 276 | 'actType': actType, 277 | 'revs': revData, 278 | 'fullSchedInfo': schedInfo 279 | }); 280 | } 281 | } 282 | } 283 | var sectionInfo = parse.SectionInfo(Res); 284 | 285 | return [sectionsList, sectionInfo]; 286 | }; 287 | 288 | parse.RecordRegistrar = function(inJSON) { 289 | var fs = require('fs'); 290 | 291 | var resp = {}; 292 | var meetresp = {}; 293 | var thisKey; 294 | for(var key in inJSON) { if (inJSON.hasOwnProperty(key)) { 295 | // For each section that comes up 296 | // Get course name (e.g. CIS 120) 297 | thisKey = inJSON[key]; 298 | var idSpaced = thisKey.course_department + ' ' + thisKey.course_number; 299 | var secID = thisKey.course_department + '-' + thisKey.course_number + '-' + thisKey.section_number; 300 | var numCred = Number(thisKey.credits.split(" ")[0]); 301 | 302 | if (!thisKey.is_cancelled) { // Don't include cancelled sections 303 | var idDashed = idSpaced.replace(' ', '-'); 304 | if (!resp[idSpaced]) { // If there is no existing record for the course, make a new record 305 | var reqCodesList = r.GetRequirements(thisKey)[0]; 306 | resp[idSpaced] = { 307 | 'idDashed': idDashed, 308 | 'idSpaced': idSpaced, 309 | 'courseTitle': thisKey.course_title, 310 | 'courseReqs': reqCodesList, 311 | 'courseCred': numCred 312 | }; 313 | } else if (resp[idSpaced].courseCred < numCred) { // If there is, make the numCred value the max 314 | resp[idSpaced].courseCred = numCred; 315 | } 316 | var meetingInfo = SectionMeeting(thisKey); 317 | meetresp[secID] = meetingInfo; 318 | } 319 | }} 320 | var arrResp = []; 321 | for (key in resp) { if (resp.hasOwnProperty(key)) { 322 | arrResp.push(resp[key]); 323 | }} 324 | if (thisKey) { 325 | var thedept = thisKey.course_department; 326 | var currentTerm = thisKey.term; 327 | // At the end of the list 328 | fs.writeFile('../Data/'+currentTerm+'/'+thedept+'.json', JSON.stringify(arrResp), function (err) { 329 | // Write JSON to file 330 | if (err) { 331 | console.log(thedept+' '+err); 332 | } else { 333 | console.log(('Reg Spit: '+thedept)); 334 | } 335 | }); 336 | fs.writeFile('../Data/'+currentTerm+'Meet/'+thedept+'.json', JSON.stringify(meetresp), function (err) { 337 | // Write JSON to file 338 | if (err) { 339 | console.log(thedept+' '+err); 340 | } else { 341 | console.log(('Meet Spit: '+thedept)); 342 | } 343 | }); 344 | } 345 | }; 346 | 347 | module.exports = parse; -------------------------------------------------------------------------------- /pcn.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/pcn.js -------------------------------------------------------------------------------- /plugins/angular-local-storage.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An Angular module that gives you access to the browsers local storage 3 | * @version v0.2.3 - 2015-10-11 4 | * @link https://github.com/grevory/angular-local-storage 5 | * @author grevory 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */!function (a, b) { 8 | "use strict"; 9 | var c = b.isDefined, 10 | d = b.isUndefined, 11 | e = b.isNumber, 12 | f = b.isObject, 13 | g = b.isArray, 14 | h = b.extend, 15 | i = b.toJson, 16 | j = b.module("LocalStorageModule", []);j.provider("localStorageService", function () { 17 | this.prefix = "ls", this.storageType = "localStorage", this.cookie = { expiry: 30, path: "/" }, this.notify = { setItem: !0, removeItem: !1 }, this.setPrefix = function (a) { 18 | return this.prefix = a, this; 19 | }, this.setStorageType = function (a) { 20 | return this.storageType = a, this; 21 | }, this.setStorageCookie = function (a, b) { 22 | return this.cookie.expiry = a, this.cookie.path = b, this; 23 | }, this.setStorageCookieDomain = function (a) { 24 | return this.cookie.domain = a, this; 25 | }, this.setNotify = function (a, b) { 26 | return this.notify = { setItem: a, removeItem: b }, this; 27 | }, this.$get = ["$rootScope", "$window", "$document", "$parse", function (a, b, j, k) { 28 | var l, 29 | m = this, 30 | n = m.prefix, 31 | o = m.cookie, 32 | p = m.notify, 33 | q = m.storageType;j ? j[0] && (j = j[0]) : j = document, "." !== n.substr(-1) && (n = n ? n + "." : "");var r = function r(a) { 34 | return n + a; 35 | }, 36 | s = function () { 37 | try { 38 | var c = q in b && null !== b[q], 39 | d = r("__" + Math.round(1e7 * Math.random()));return c && (l = b[q], l.setItem(d, ""), l.removeItem(d)), c; 40 | } catch (e) { 41 | return q = "cookie", a.$broadcast("LocalStorageModule.notification.error", e.message), !1; 42 | } 43 | }(), 44 | t = function t(b, c) { 45 | if (c = d(c) ? null : i(c), !s || "cookie" === m.storageType) return s || a.$broadcast("LocalStorageModule.notification.warning", "LOCAL_STORAGE_NOT_SUPPORTED"), p.setItem && a.$broadcast("LocalStorageModule.notification.setitem", { key: b, newvalue: c, storageType: "cookie" }), z(b, c);try { 46 | l && l.setItem(r(b), c), p.setItem && a.$broadcast("LocalStorageModule.notification.setitem", { key: b, newvalue: c, storageType: m.storageType }); 47 | } catch (e) { 48 | return a.$broadcast("LocalStorageModule.notification.error", e.message), z(b, c); 49 | }return !0; 50 | }, 51 | u = function u(b) { 52 | if (!s || "cookie" === m.storageType) return s || a.$broadcast("LocalStorageModule.notification.warning", "LOCAL_STORAGE_NOT_SUPPORTED"), A(b);var c = l ? l.getItem(r(b)) : null;if (!c || "null" === c) return null;try { 53 | return JSON.parse(c); 54 | } catch (d) { 55 | return c; 56 | } 57 | }, 58 | v = function v() { 59 | var b, c;for (b = 0; b < arguments.length; b++) { 60 | if (c = arguments[b], s && "cookie" !== m.storageType) try { 61 | l.removeItem(r(c)), p.removeItem && a.$broadcast("LocalStorageModule.notification.removeitem", { key: c, storageType: m.storageType }); 62 | } catch (d) { 63 | a.$broadcast("LocalStorageModule.notification.error", d.message), B(c); 64 | } else s || a.$broadcast("LocalStorageModule.notification.warning", "LOCAL_STORAGE_NOT_SUPPORTED"), p.removeItem && a.$broadcast("LocalStorageModule.notification.removeitem", { key: c, storageType: "cookie" }), B(c); 65 | } 66 | }, 67 | w = function w() { 68 | if (!s) return a.$broadcast("LocalStorageModule.notification.warning", "LOCAL_STORAGE_NOT_SUPPORTED"), !1;var b = n.length, 69 | c = [];for (var d in l) { 70 | if (d.substr(0, b) === n) try { 71 | c.push(d.substr(b)); 72 | } catch (e) { 73 | return a.$broadcast("LocalStorageModule.notification.error", e.Description), []; 74 | } 75 | }return c; 76 | }, 77 | x = function x(b) { 78 | var c = n ? new RegExp("^" + n) : new RegExp(), 79 | d = b ? new RegExp(b) : new RegExp();if (!s || "cookie" === m.storageType) return s || a.$broadcast("LocalStorageModule.notification.warning", "LOCAL_STORAGE_NOT_SUPPORTED"), C();var e = n.length;for (var f in l) { 80 | if (c.test(f) && d.test(f.substr(e))) try { 81 | v(f.substr(e)); 82 | } catch (g) { 83 | return a.$broadcast("LocalStorageModule.notification.error", g.message), C(); 84 | } 85 | }return !0; 86 | }, 87 | y = function () { 88 | try { 89 | return b.navigator.cookieEnabled || "cookie" in j && (j.cookie.length > 0 || (j.cookie = "test").indexOf.call(j.cookie, "test") > -1); 90 | } catch (c) { 91 | return a.$broadcast("LocalStorageModule.notification.error", c.message), !1; 92 | } 93 | }(), 94 | z = function z(b, c, h) { 95 | if (d(c)) return !1;if ((g(c) || f(c)) && (c = i(c)), !y) return a.$broadcast("LocalStorageModule.notification.error", "COOKIES_NOT_SUPPORTED"), !1;try { 96 | var k = "", 97 | l = new Date(), 98 | m = "";if (null === c ? (l.setTime(l.getTime() + -864e5), k = "; expires=" + l.toGMTString(), c = "") : e(h) && 0 !== h ? (l.setTime(l.getTime() + 24 * h * 60 * 60 * 1e3), k = "; expires=" + l.toGMTString()) : 0 !== o.expiry && (l.setTime(l.getTime() + 24 * o.expiry * 60 * 60 * 1e3), k = "; expires=" + l.toGMTString()), b) { 99 | var n = "; path=" + o.path;o.domain && (m = "; domain=" + o.domain), j.cookie = r(b) + "=" + encodeURIComponent(c) + k + n + m; 100 | } 101 | } catch (p) { 102 | return a.$broadcast("LocalStorageModule.notification.error", p.message), !1; 103 | }return !0; 104 | }, 105 | A = function A(b) { 106 | if (!y) return a.$broadcast("LocalStorageModule.notification.error", "COOKIES_NOT_SUPPORTED"), !1;for (var c = j.cookie && j.cookie.split(";") || [], d = 0; d < c.length; d++) { 107 | for (var e = c[d]; " " === e.charAt(0);) { 108 | e = e.substring(1, e.length); 109 | }if (0 === e.indexOf(r(b) + "=")) { 110 | var f = decodeURIComponent(e.substring(n.length + b.length + 1, e.length));try { 111 | return JSON.parse(f); 112 | } catch (g) { 113 | return f; 114 | } 115 | } 116 | }return null; 117 | }, 118 | B = function B(a) { 119 | z(a, null); 120 | }, 121 | C = function C() { 122 | for (var a = null, b = n.length, c = j.cookie.split(";"), d = 0; d < c.length; d++) { 123 | for (a = c[d]; " " === a.charAt(0);) { 124 | a = a.substring(1, a.length); 125 | }var e = a.substring(b, a.indexOf("="));B(e); 126 | } 127 | }, 128 | D = function D() { 129 | return q; 130 | }, 131 | E = function E(a, b, d, e) { 132 | e = e || b;var g = u(e);return null === g && c(d) ? g = d : f(g) && f(d) && (g = h(d, g)), k(b).assign(a, g), a.$watch(b, function (a) { 133 | t(e, a); 134 | }, f(a[b])); 135 | }, 136 | F = function F() { 137 | for (var a = 0, c = b[q], d = 0; d < c.length; d++) { 138 | 0 === c.key(d).indexOf(n) && a++; 139 | }return a; 140 | };return { isSupported: s, getStorageType: D, set: t, add: t, get: u, keys: w, remove: v, clearAll: x, bind: E, deriveKey: r, length: F, cookie: { isSupported: y, set: z, add: z, get: A, remove: B, clearAll: C } }; 141 | }]; 142 | }); 143 | }(window, window.angular); -------------------------------------------------------------------------------- /plugins/angular-tooltips.min.js: -------------------------------------------------------------------------------- 1 | !function () { 2 | "use strict"; 3 | var t = function t(_t) { 4 | return { restrict: "A", scope: !0, link: function link(e, o, i) { 5 | (i.title || i.tooltip) && (_t(function () { 6 | o.removeAttr("ng-attr-title"), o.removeAttr("title"); 7 | }), o.on("mouseover", function (t) { 8 | var o = e.getDirection(), 9 | l = angular.element("
").addClass("angular-tooltip angular-tooltip-" + o).html(i.title || i.tooltip);angular.element(document).find("body").append(l);var r = e.calculatePosition(l, o);l.css(r), l.addClass("angular-tooltip-fade-in"); 10 | }), e.removeTooltip = function () { 11 | var e = angular.element(document.querySelectorAll(".angular-tooltip"));e.removeClass("angular-tooltip-fade-in"), _t(function () { 12 | e.remove(); 13 | }, 300); 14 | }, e.getDirection = function () { 15 | return o.attr("tooltip-direction") || o.attr("title-direction") || "top"; 16 | }, e.calculatePosition = function (t, e) { 17 | var i = t[0].getBoundingClientRect(), 18 | l = o[0].getBoundingClientRect(), 19 | r = window.scrollX || document.documentElement.scrollLeft, 20 | n = window.scrollY || document.documentElement.scrollTop, 21 | p = 12;switch (e) {case "top":case "top-center":case "top-middle": 22 | return { left: l.left + l.width / 2 - i.width / 2 + r + "px", top: l.top - i.height - p / 2 + n + "px" };case "top-right": 23 | return { left: l.left + l.width - p + r + "px", top: l.top - i.height - p / 2 + n + "px" };case "right-top": 24 | return { left: l.left + l.width + p / 2 + r + "px", top: l.top - i.height + p + n + "px" };case "right":case "right-center":case "right-middle": 25 | return { left: l.left + l.width + p / 2 + r + "px", top: l.top + l.height / 2 - i.height / 2 + n + "px" };case "right-bottom": 26 | return { left: l.left + l.width + p / 2 + r + "px", top: l.top + l.height - p + n + "px" };case "bottom-right": 27 | return { left: l.left + l.width - p + r + "px", top: l.top + l.height + p / 2 + n + "px" };case "bottom":case "bottom-center":case "bottom-middle": 28 | return { left: l.left + l.width / 2 - i.width / 2 + r + "px", top: l.top + l.height + p / 2 + n + "px" };case "bottom-left": 29 | return { left: l.left - i.width + p + r + "px", top: l.top + l.height + p / 2 + n + "px" };case "left-bottom": 30 | return { left: l.left - i.width - p / 2 + r + "px", top: l.top + l.height - p + n + "px" };case "left":case "left-center":case "left-middle": 31 | return { left: l.left - i.width - p / 2 + r + "px", top: l.top + l.height / 2 - i.height / 2 + n + "px" };case "left-top": 32 | return { left: l.left - i.width - p / 2 + r + "px", top: l.top - i.height + p + n + "px" };case "top-left": 33 | return { left: l.left - i.width + p + r + "px", top: l.top - i.height - p / 2 + n + "px" };} 34 | }, o.on("mouseout", e.removeTooltip), o.on("destroy", e.removeTooltip), e.$on("$destroy", e.removeTooltip)); 35 | } }; 36 | };t.$inject = ["$timeout"], angular.module("tooltips", []).directive("title", t).directive("tooltip", t); 37 | }(); -------------------------------------------------------------------------------- /plugins/jquery.leanModal.min.js: -------------------------------------------------------------------------------- 1 | // leanModal v1.1 by Ray Stone - http://finelysliced.com.au 2 | // Dual licensed under the MIT and GPL 3 | !function (e) { 4 | e.fn.extend({ leanModal: function leanModal(n) { 5 | function o(n) { 6 | e("#lean_overlay").fadeOut(200), e(n).css({ display: "none" }); 7 | }var t = { top: 100, overlay: .5, closeButton: null }, 8 | a = e("
");return e("body").append(a), n = e.extend(t, n), this.each(function () { 9 | var t = n;e(this).click(function (n) { 10 | var a = e(this).attr("href");e("#lean_overlay").click(function () { 11 | o(a); 12 | }), e(t.closeButton).click(function () { 13 | o(a); 14 | });var l = (e(a).outerHeight(), e(a).outerWidth());e("#lean_overlay").css({ display: "block", opacity: 0 }), e("#lean_overlay").fadeTo(200, t.overlay), e(a).css({ display: "block", position: "fixed", opacity: 0, "z-index": 11e3, left: "50%", "margin-left": -(l / 2) + "px", top: t.top + "px" }), e(a).fadeTo(200, 1), n.preventDefault(); 15 | }); 16 | }); 17 | } }); 18 | }(jQuery); -------------------------------------------------------------------------------- /public/Import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/Import.png -------------------------------------------------------------------------------- /public/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/Logo.png -------------------------------------------------------------------------------- /public/css/blue_heart_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/css/blue_heart_2.png -------------------------------------------------------------------------------- /public/css/filter_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/css/filter_a.png -------------------------------------------------------------------------------- /public/css/filter_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/css/filter_b.png -------------------------------------------------------------------------------- /public/css/graphic.svg: -------------------------------------------------------------------------------- 1 | notebook -------------------------------------------------------------------------------- /public/css/plugins/angular-tooltips.min.css: -------------------------------------------------------------------------------- 1 | .angular-tooltip{transition:opacity .5s;opacity:0;position:absolute;background:#000;z-index:9999;padding:3px;border:2px solid #000;color:#FFF;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;max-width:250px;border-radius:3px}.angular-tooltip.angular-tooltip-fade-in{opacity:1}.angular-tooltip:after,.angular-tooltip:before{border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.angular-tooltip:before{border-color:transparent;border-width:8px}.angular-tooltip:after{border-color:transparent;border-width:5px}.angular-tooltip.angular-tooltip-top-center:after,.angular-tooltip.angular-tooltip-top-center:before,.angular-tooltip.angular-tooltip-top-middle:after,.angular-tooltip.angular-tooltip-top-middle:before,.angular-tooltip.angular-tooltip-top:after,.angular-tooltip.angular-tooltip-top:before{top:100%;left:50%}.angular-tooltip.angular-tooltip-top-center:before,.angular-tooltip.angular-tooltip-top-middle:before,.angular-tooltip.angular-tooltip-top:before{margin-left:-8px;border-top-color:#000}.angular-tooltip.angular-tooltip-top-center:after,.angular-tooltip.angular-tooltip-top-middle:after,.angular-tooltip.angular-tooltip-top:after{margin-left:-5px;border-top-color:#000}.angular-tooltip.angular-tooltip-top-right:after,.angular-tooltip.angular-tooltip-top-right:before{top:100%;right:calc(100% - 8px)}.angular-tooltip.angular-tooltip-top-right:before{margin-right:-8px;border-top-color:#000}.angular-tooltip.angular-tooltip-top-right:after{margin-right:-5px;border-top-color:#000}.angular-tooltip.angular-tooltip-right-top:after,.angular-tooltip.angular-tooltip-right-top:before{top:calc(100% - 8px);right:100%}.angular-tooltip.angular-tooltip-right-top:before{margin-top:-8px;border-right-color:#000}.angular-tooltip.angular-tooltip-right-top:after{margin-top:-5px;border-right-color:#000}.angular-tooltip.angular-tooltip-right-center:after,.angular-tooltip.angular-tooltip-right-center:before,.angular-tooltip.angular-tooltip-right-middle:after,.angular-tooltip.angular-tooltip-right-middle:before,.angular-tooltip.angular-tooltip-right:after,.angular-tooltip.angular-tooltip-right:before{top:50%;right:100%}.angular-tooltip.angular-tooltip-right-center:before,.angular-tooltip.angular-tooltip-right-middle:before,.angular-tooltip.angular-tooltip-right:before{margin-top:-8px;border-right-color:#000}.angular-tooltip.angular-tooltip-right-center:after,.angular-tooltip.angular-tooltip-right-middle:after,.angular-tooltip.angular-tooltip-right:after{margin-top:-5px;border-right-color:#000}.angular-tooltip.angular-tooltip-right-bottom:after,.angular-tooltip.angular-tooltip-right-bottom:before{bottom:calc(100% - 8px);right:100%}.angular-tooltip.angular-tooltip-right-bottom:before{margin-bottom:-8px;border-right-color:#000}.angular-tooltip.angular-tooltip-right-bottom:after{margin-bottom:-5px;border-right-color:#000}.angular-tooltip.angular-tooltip-bottom-right:after,.angular-tooltip.angular-tooltip-bottom-right:before{bottom:100%;right:calc(100% - 8px)}.angular-tooltip.angular-tooltip-bottom-right:before{margin-right:-8px;border-bottom-color:#000}.angular-tooltip.angular-tooltip-bottom-right:after{margin-right:-5px;border-bottom-color:#000}.angular-tooltip.angular-tooltip-bottom-center:after,.angular-tooltip.angular-tooltip-bottom-center:before,.angular-tooltip.angular-tooltip-bottom-middle:after,.angular-tooltip.angular-tooltip-bottom-middle:before,.angular-tooltip.angular-tooltip-bottom:after,.angular-tooltip.angular-tooltip-bottom:before{bottom:100%;left:50%}.angular-tooltip.angular-tooltip-bottom-center:before,.angular-tooltip.angular-tooltip-bottom-middle:before,.angular-tooltip.angular-tooltip-bottom:before{margin-left:-8px;border-bottom-color:#000}.angular-tooltip.angular-tooltip-bottom-center:after,.angular-tooltip.angular-tooltip-bottom-middle:after,.angular-tooltip.angular-tooltip-bottom:after{margin-left:-5px;border-bottom-color:#000}.angular-tooltip.angular-tooltip-bottom-left:after,.angular-tooltip.angular-tooltip-bottom-left:before{bottom:100%;left:calc(100% - 8px)}.angular-tooltip.angular-tooltip-bottom-left:before{margin-left:-8px;border-bottom-color:#000}.angular-tooltip.angular-tooltip-bottom-left:after{margin-left:-5px;border-bottom-color:#000}.angular-tooltip.angular-tooltip-left-bottom:after,.angular-tooltip.angular-tooltip-left-bottom:before{bottom:calc(100% - 8px);left:100%}.angular-tooltip.angular-tooltip-left-bottom:before{margin-bottom:-8px;border-left-color:#000}.angular-tooltip.angular-tooltip-left-bottom:after{margin-bottom:-5px;border-left-color:#000}.angular-tooltip.angular-tooltip-left-center:after,.angular-tooltip.angular-tooltip-left-center:before,.angular-tooltip.angular-tooltip-left-middle:after,.angular-tooltip.angular-tooltip-left-middle:before,.angular-tooltip.angular-tooltip-left:after,.angular-tooltip.angular-tooltip-left:before{top:50%;left:100%}.angular-tooltip.angular-tooltip-left-center:before,.angular-tooltip.angular-tooltip-left-middle:before,.angular-tooltip.angular-tooltip-left:before{margin-top:-8px;border-left-color:#000}.angular-tooltip.angular-tooltip-left-center:after,.angular-tooltip.angular-tooltip-left-middle:after,.angular-tooltip.angular-tooltip-left:after{margin-top:-5px;border-left-color:#000}.angular-tooltip.angular-tooltip-left-top:after,.angular-tooltip.angular-tooltip-left-top:before{top:calc(100% - 8px);left:100%}.angular-tooltip.angular-tooltip-left-top:before{margin-top:-8px;border-left-color:#000}.angular-tooltip.angular-tooltip-left-top:after{margin-top:-5px;border-left-color:#000}.angular-tooltip.angular-tooltip-top-left:after,.angular-tooltip.angular-tooltip-top-left:before{top:100%;left:calc(100% - 8px)}.angular-tooltip.angular-tooltip-top-left:before{margin-left:-8px;border-top-color:#000}.angular-tooltip.angular-tooltip-top-left:after{margin-left:-5px;border-top-color:#000} -------------------------------------------------------------------------------- /public/css/plugins/sweetalert.css: -------------------------------------------------------------------------------- 1 | .sweet-alert,.sweet-overlay{position:fixed;display:none}body.stop-scrolling{height:100%;overflow:hidden}.sweet-overlay{background-color:#000;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";background-color:rgba(0,0,0,.4);left:0;right:0;top:0;bottom:0;z-index:10000}.sweet-alert{background-color:#fff;font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif;width:478px;padding:17px;border-radius:5px;text-align:center;left:50%;top:50%;margin-left:-256px;margin-top:-200px;overflow:hidden;z-index:99999}@media all and (max-width:540px){.sweet-alert{width:auto;margin-left:0;margin-right:0;left:15px;right:15px}}.sweet-alert h2{color:#575757;font-size:30px;text-align:center;font-weight:600;text-transform:none;position:relative;margin:25px 0;padding:0;line-height:40px;display:block}.sweet-alert p{color:#797979;font-size:16px;font-weight:300;position:relative;text-align:inherit;float:none;margin:0;padding:0;line-height:normal}.sweet-alert fieldset{border:none;position:relative}.sweet-alert .sa-error-container{background-color:#f1f1f1;margin-left:-17px;margin-right:-17px;overflow:hidden;padding:0 10px;max-height:0;webkit-transition:padding .15s,max-height .15s;transition:padding .15s,max-height .15s}.sweet-alert .sa-error-container.show{padding:10px 0;max-height:100px;webkit-transition:padding .2s,max-height .2s;transition:padding .25s,max-height .25s}.sweet-alert .sa-error-container .icon{display:inline-block;width:24px;height:24px;border-radius:50%;background-color:#ea7d7d;color:#fff;line-height:24px;text-align:center;margin-right:3px}.sweet-alert .sa-error-container p{display:inline-block}.sweet-alert .sa-input-error{position:absolute;top:29px;right:26px;width:20px;height:20px;opacity:0;-webkit-transform:scale(.5);transform:scale(.5);-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transition:all .1s;transition:all .1s}.sweet-alert .sa-input-error::after,.sweet-alert .sa-input-error::before{content:"";width:20px;height:6px;background-color:#f06e57;border-radius:3px;position:absolute;top:50%;margin-top:-4px;left:50%;margin-left:-9px}.sweet-alert .sa-input-error::before{-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.sweet-alert .sa-input-error::after{-webkit-transform:rotate(45deg);transform:rotate(45deg)}.sweet-alert .sa-input-error.show{opacity:1;-webkit-transform:scale(1);transform:scale(1)}.sweet-alert input{width:100%;box-sizing:border-box;border-radius:3px;border:1px solid #d7d7d7;height:43px;margin-top:10px;margin-bottom:17px;font-size:18px;box-shadow:inset 0 1px 1px rgba(0,0,0,.06);padding:0 12px;display:none;-webkit-transition:all .3s;transition:all .3s}.sweet-alert input:focus{outline:0;box-shadow:0 0 3px #c4e6f5;border:1px solid #b4dbed}.sweet-alert input:focus::-moz-placeholder{transition:opacity .3s 30ms ease;opacity:.5}.sweet-alert input:focus:-ms-input-placeholder{transition:opacity .3s 30ms ease;opacity:.5}.sweet-alert input:focus::-webkit-input-placeholder{transition:opacity .3s 30ms ease;opacity:.5}.sweet-alert input::-moz-placeholder{color:#bdbdbd}.sweet-alert input:-ms-input-placeholder{color:#bdbdbd}.sweet-alert input::-webkit-input-placeholder{color:#bdbdbd}.sweet-alert.show-input input{display:block}.sweet-alert .sa-confirm-button-container{display:inline-block;position:relative}.sweet-alert .la-ball-fall{position:absolute;left:50%;top:50%;margin-left:-27px;margin-top:4px;opacity:0;visibility:hidden}.sweet-alert button{background-color:#8CD4F5;color:#fff;border:none;box-shadow:none;font-size:17px;font-weight:500;-webkit-border-radius:4px;border-radius:5px;padding:10px 32px;margin:26px 5px 0;cursor:pointer}.sweet-alert button:focus{outline:0;box-shadow:0 0 2px rgba(128,179,235,.5),inset 0 0 0 1px rgba(0,0,0,.05)}.sweet-alert button:hover{background-color:#7ecff4}.sweet-alert button:active{background-color:#5dc2f1}.sweet-alert button.cancel{background-color:#C1C1C1}.sweet-alert button.cancel:hover{background-color:#b9b9b9}.sweet-alert button.cancel:active{background-color:#a8a8a8}.sweet-alert button.cancel:focus{box-shadow:rgba(197,205,211,.8) 0 0 2px,rgba(0,0,0,.0470588) 0 0 0 1px inset!important}.sweet-alert button[disabled]{opacity:.6;cursor:default}.sweet-alert button.confirm[disabled]{color:transparent}.sweet-alert button.confirm[disabled]~.la-ball-fall{opacity:1;visibility:visible;transition-delay:0s}.sweet-alert button::-moz-focus-inner{border:0}.sweet-alert[data-has-cancel-button=false] button{box-shadow:none!important}.sweet-alert[data-has-confirm-button=false][data-has-cancel-button=false]{padding-bottom:40px}.sweet-alert .sa-icon{width:80px;height:80px;border:4px solid gray;-webkit-border-radius:40px;border-radius:50%;margin:20px auto;padding:0;position:relative;box-sizing:content-box}.sweet-alert .sa-icon.sa-error{border-color:#F27474}.sweet-alert .sa-icon.sa-error .sa-x-mark{position:relative;display:block}.sweet-alert .sa-icon.sa-error .sa-line{position:absolute;height:5px;width:47px;background-color:#F27474;display:block;top:37px;border-radius:2px}.sweet-alert .sa-icon.sa-error .sa-line.sa-left{-webkit-transform:rotate(45deg);transform:rotate(45deg);left:17px}.sweet-alert .sa-icon.sa-error .sa-line.sa-right{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);right:16px}.sweet-alert .sa-icon.sa-warning{border-color:#F8BB86}.sweet-alert .sa-icon.sa-warning .sa-body{position:absolute;width:5px;height:47px;left:50%;top:10px;-webkit-border-radius:2px;border-radius:2px;margin-left:-2px;background-color:#F8BB86}.sweet-alert .sa-icon.sa-warning .sa-dot{position:absolute;width:7px;height:7px;-webkit-border-radius:50%;border-radius:50%;margin-left:-3px;left:50%;bottom:10px;background-color:#F8BB86}.sweet-alert .sa-icon.sa-info::after,.sweet-alert .sa-icon.sa-info::before{content:"";background-color:#C9DAE1;position:absolute}.sweet-alert .sa-icon.sa-info{border-color:#C9DAE1}.sweet-alert .sa-icon.sa-info::before{width:5px;height:29px;left:50%;bottom:17px;border-radius:2px;margin-left:-2px}.sweet-alert .sa-icon.sa-info::after{width:7px;height:7px;border-radius:50%;margin-left:-3px;top:19px}.sweet-alert .sa-icon.sa-success{border-color:#A5DC86}.sweet-alert .sa-icon.sa-success::after,.sweet-alert .sa-icon.sa-success::before{content:'';position:absolute;width:60px;height:120px;background:#fff}.sweet-alert .sa-icon.sa-success::before{-webkit-border-radius:120px 0 0 120px;border-radius:120px 0 0 120px;top:-7px;left:-33px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:60px 60px;transform-origin:60px 60px}.sweet-alert .sa-icon.sa-success::after{-webkit-border-radius:0 120px 120px 0;border-radius:0 120px 120px 0;top:-11px;left:30px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);-webkit-transform-origin:0 60px;transform-origin:0 60px}.sweet-alert .sa-icon.sa-success .sa-placeholder{width:80px;height:80px;border:4px solid rgba(165,220,134,.2);-webkit-border-radius:40px;border-radius:50%;box-sizing:content-box;position:absolute;left:-4px;top:-4px;z-index:2}.sweet-alert .sa-icon.sa-success .sa-fix{width:5px;height:90px;background-color:#fff;position:absolute;left:28px;top:8px;z-index:1;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.sweet-alert .sa-icon.sa-success .sa-line{height:5px;background-color:#A5DC86;display:block;border-radius:2px;position:absolute;z-index:2}.sweet-alert .sa-icon.sa-success .sa-line.sa-tip{width:25px;left:14px;top:46px;-webkit-transform:rotate(45deg);transform:rotate(45deg)}.sweet-alert .sa-icon.sa-success .sa-line.sa-long{width:47px;right:8px;top:38px;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.sweet-alert .sa-icon.sa-custom{background-size:contain;border-radius:0;border:none;background-position:center center;background-repeat:no-repeat}@-webkit-keyframes showSweetAlert{0%{transform:scale(.7);-webkit-transform:scale(.7)}45%{transform:scale(1.05);-webkit-transform:scale(1.05)}80%{transform:scale(.95);-webkit-transform:scale(.95)}100%{transform:scale(1);-webkit-transform:scale(1)}}@keyframes showSweetAlert{0%{transform:scale(.7);-webkit-transform:scale(.7)}45%{transform:scale(1.05);-webkit-transform:scale(1.05)}80%{transform:scale(.95);-webkit-transform:scale(.95)}100%{transform:scale(1);-webkit-transform:scale(1)}}@-webkit-keyframes hideSweetAlert{0%{transform:scale(1);-webkit-transform:scale(1)}100%{transform:scale(.5);-webkit-transform:scale(.5)}}@keyframes hideSweetAlert{0%{transform:scale(1);-webkit-transform:scale(1)}100%{transform:scale(.5);-webkit-transform:scale(.5)}}@-webkit-keyframes slideFromTop{0%{top:0}100%{top:50%}}@keyframes slideFromTop{0%{top:0}100%{top:50%}}@-webkit-keyframes slideToTop{0%{top:50%}100%{top:0}}@keyframes slideToTop{0%{top:50%}100%{top:0}}@-webkit-keyframes slideFromBottom{0%{top:70%}100%{top:50%}}@keyframes slideFromBottom{0%{top:70%}100%{top:50%}}@-webkit-keyframes slideToBottom{0%{top:50%}100%{top:70%}}@keyframes slideToBottom{0%{top:50%}100%{top:70%}}.showSweetAlert[data-animation=pop]{-webkit-animation:showSweetAlert .3s;animation:showSweetAlert .3s}.showSweetAlert[data-animation=none]{-webkit-animation:none;animation:none}.showSweetAlert[data-animation=slide-from-top]{-webkit-animation:slideFromTop .3s;animation:slideFromTop .3s}.showSweetAlert[data-animation=slide-from-bottom]{-webkit-animation:slideFromBottom .3s;animation:slideFromBottom .3s}.hideSweetAlert[data-animation=pop]{-webkit-animation:hideSweetAlert .2s;animation:hideSweetAlert .2s}.hideSweetAlert[data-animation=none]{-webkit-animation:none;animation:none}.hideSweetAlert[data-animation=slide-from-top]{-webkit-animation:slideToTop .4s;animation:slideToTop .4s}.hideSweetAlert[data-animation=slide-from-bottom]{-webkit-animation:slideToBottom .3s;animation:slideToBottom .3s}@-webkit-keyframes animateSuccessTip{0%,54%{width:0;left:1px;top:19px}70%{width:50px;left:-8px;top:37px}84%{width:17px;left:21px;top:48px}100%{width:25px;left:14px;top:45px}}@keyframes animateSuccessTip{0%,54%{width:0;left:1px;top:19px}70%{width:50px;left:-8px;top:37px}84%{width:17px;left:21px;top:48px}100%{width:25px;left:14px;top:45px}}@-webkit-keyframes animateSuccessLong{0%,65%{width:0;right:46px;top:54px}84%{width:55px;right:0;top:35px}100%{width:47px;right:8px;top:38px}}@keyframes animateSuccessLong{0%,65%{width:0;right:46px;top:54px}84%{width:55px;right:0;top:35px}100%{width:47px;right:8px;top:38px}}@-webkit-keyframes rotatePlaceholder{0%,5%{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}100%,12%{transform:rotate(-405deg);-webkit-transform:rotate(-405deg)}}@keyframes rotatePlaceholder{0%,5%{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}100%,12%{transform:rotate(-405deg);-webkit-transform:rotate(-405deg)}}.animateSuccessTip{-webkit-animation:animateSuccessTip .75s;animation:animateSuccessTip .75s}.animateSuccessLong{-webkit-animation:animateSuccessLong .75s;animation:animateSuccessLong .75s}.sa-icon.sa-success.animate::after{-webkit-animation:rotatePlaceholder 4.25s ease-in;animation:rotatePlaceholder 4.25s ease-in}@-webkit-keyframes animateErrorIcon{0%{transform:rotateX(100deg);-webkit-transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0);-webkit-transform:rotateX(0);opacity:1}}@keyframes animateErrorIcon{0%{transform:rotateX(100deg);-webkit-transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0);-webkit-transform:rotateX(0);opacity:1}}.animateErrorIcon{-webkit-animation:animateErrorIcon .5s;animation:animateErrorIcon .5s}@-webkit-keyframes animateXMark{0%,50%{transform:scale(.4);-webkit-transform:scale(.4);margin-top:26px;opacity:0}80%{transform:scale(1.15);-webkit-transform:scale(1.15);margin-top:-6px}100%{transform:scale(1);-webkit-transform:scale(1);margin-top:0;opacity:1}}@keyframes animateXMark{0%,50%{transform:scale(.4);-webkit-transform:scale(.4);margin-top:26px;opacity:0}80%{transform:scale(1.15);-webkit-transform:scale(1.15);margin-top:-6px}100%{transform:scale(1);-webkit-transform:scale(1);margin-top:0;opacity:1}}.animateXMark{-webkit-animation:animateXMark .5s;animation:animateXMark .5s}@-webkit-keyframes pulseWarning{0%{border-color:#F8D486}100%{border-color:#F8BB86}}@keyframes pulseWarning{0%{border-color:#F8D486}100%{border-color:#F8BB86}}.pulseWarning{-webkit-animation:pulseWarning .75s infinite alternate;animation:pulseWarning .75s infinite alternate}@-webkit-keyframes pulseWarningIns{0%{background-color:#F8D486}100%{background-color:#F8BB86}}@keyframes pulseWarningIns{0%{background-color:#F8D486}100%{background-color:#F8BB86}}.pulseWarningIns{-webkit-animation:pulseWarningIns .75s infinite alternate;animation:pulseWarningIns .75s infinite alternate}@-webkit-keyframes rotate-loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}@keyframes rotate-loading{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}.sweet-alert .sa-icon.sa-error .sa-line.sa-left{-ms-transform:rotate(45deg)\9}.sweet-alert .sa-icon.sa-error .sa-line.sa-right{-ms-transform:rotate(-45deg)\9}.sweet-alert .sa-icon.sa-success{border-color:transparent\9}.sweet-alert .sa-icon.sa-success .sa-line.sa-tip{-ms-transform:rotate(45deg)\9}.sweet-alert .sa-icon.sa-success .sa-line.sa-long{-ms-transform:rotate(-45deg)\9}/*! 2 | * Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/) 3 | * Copyright 2015 Daniel Cardoso <@DanielCardoso> 4 | * Licensed under MIT 5 | */ 6 | .la-ball-fall,.la-ball-fall>div{position:relative;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.la-ball-fall{display:block;font-size:0;color:#fff;width:54px;height:18px}.la-ball-fall.la-dark{color:#333}.la-ball-fall>div{display:inline-block;float:none;background-color:currentColor;border:0 solid currentColor;width:10px;height:10px;margin:4px;border-radius:100%;opacity:0;-webkit-animation:ball-fall 1s ease-in-out infinite;-moz-animation:ball-fall 1s ease-in-out infinite;-o-animation:ball-fall 1s ease-in-out infinite;animation:ball-fall 1s ease-in-out infinite}.la-ball-fall>div:nth-child(1){-webkit-animation-delay:-.2s;-moz-animation-delay:-.2s;-o-animation-delay:-.2s;animation-delay:-.2s}.la-ball-fall>div:nth-child(2){-webkit-animation-delay:-.1s;-moz-animation-delay:-.1s;-o-animation-delay:-.1s;animation-delay:-.1s}.la-ball-fall>div:nth-child(3){-webkit-animation-delay:0s;-moz-animation-delay:0s;-o-animation-delay:0s;animation-delay:0s}.la-ball-fall.la-sm{width:26px;height:8px}.la-ball-fall.la-sm>div{width:4px;height:4px;margin:2px}.la-ball-fall.la-2x{width:108px;height:36px}.la-ball-fall.la-2x>div{width:20px;height:20px;margin:8px}.la-ball-fall.la-3x{width:162px;height:54px}.la-ball-fall.la-3x>div{width:30px;height:30px;margin:12px}@-webkit-keyframes ball-fall{0%{opacity:0;-webkit-transform:translateY(-145%);transform:translateY(-145%)}10%,90%{opacity:.5}20%,80%{opacity:1;-webkit-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(145%);transform:translateY(145%)}}@-moz-keyframes ball-fall{0%{opacity:0;-moz-transform:translateY(-145%);transform:translateY(-145%)}10%,90%{opacity:.5}20%,80%{opacity:1;-moz-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-moz-transform:translateY(145%);transform:translateY(145%)}}@-o-keyframes ball-fall{0%{opacity:0;-o-transform:translateY(-145%);transform:translateY(-145%)}10%,90%{opacity:.5}20%,80%{opacity:1;-o-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-o-transform:translateY(145%);transform:translateY(145%)}}@keyframes ball-fall{0%{opacity:0;-webkit-transform:translateY(-145%);-moz-transform:translateY(-145%);-o-transform:translateY(-145%);transform:translateY(-145%)}10%,90%{opacity:.5}20%,80%{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}100%{opacity:0;-webkit-transform:translateY(145%);-moz-transform:translateY(145%);-o-transform:translateY(145%);transform:translateY(145%)}} -------------------------------------------------------------------------------- /public/css/sparkles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/css/sparkles.png -------------------------------------------------------------------------------- /public/css/venmo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/css/venmo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pennlabs/PennCourseSearch/6cfc0b6bd82d039f508e32cd368fd59524b112d3/public/favicon.ico -------------------------------------------------------------------------------- /public/js/after_load.js: -------------------------------------------------------------------------------- 1 | //runs after the page is loaded 2 | 3 | //shows update modal on first open of updated site 4 | if(localStorage.lastUpdateNotification === undefined || localStorage.lastUpdateNotification !== "2.0"){ 5 | activate_modal(document.getElementById("NotificationModal")); 6 | localStorage.lastUpdateNotification = "2.0"; 7 | } -------------------------------------------------------------------------------- /public/js/dropdown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 8 | 9 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 10 | 11 | var OutClickable = function (_React$Component) { 12 | _inherits(OutClickable, _React$Component); 13 | 14 | // a component that you can "click out" of 15 | //requires that ref={this.setWrapperRef} is added as an attribute 16 | function OutClickable(props) { 17 | _classCallCheck(this, OutClickable); 18 | 19 | var _this = _possibleConstructorReturn(this, (OutClickable.__proto__ || Object.getPrototypeOf(OutClickable)).call(this, props)); 20 | 21 | _this.setWrapperRef = _this.setWrapperRef.bind(_this); 22 | _this.handleClickOutside = _this.handleClickOutside.bind(_this); 23 | document.addEventListener("click", _this.handleClickOutside); 24 | return _this; 25 | } 26 | 27 | /** 28 | * Alert if clicked on outside of element 29 | */ 30 | 31 | 32 | _createClass(OutClickable, [{ 33 | key: "handleClickOutside", 34 | value: function handleClickOutside(event) { 35 | if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { 36 | this.close_dropdown(); 37 | } 38 | } 39 | }, { 40 | key: "setWrapperRef", 41 | value: function setWrapperRef(node) { 42 | this.wrapperRef = node; 43 | } 44 | }]); 45 | 46 | return OutClickable; 47 | }(React.Component); 48 | 49 | var Dropdown = function (_OutClickable) { 50 | _inherits(Dropdown, _OutClickable); 51 | 52 | function Dropdown(props) { 53 | _classCallCheck(this, Dropdown); 54 | 55 | var _this2 = _possibleConstructorReturn(this, (Dropdown.__proto__ || Object.getPrototypeOf(Dropdown)).call(this, props)); 56 | 57 | var starting_activity = -1; 58 | //if props.def_active is not defined, it is assumed that the dropdown does not control 59 | //state and instead initiates an action 60 | if (props.def_active !== undefined) { 61 | starting_activity = props.def_active; 62 | } 63 | _this2.state = { active: false, activity: starting_activity, label_text: props.def_text }; 64 | _this2.activate_dropdown = _this2.activate_dropdown.bind(_this2); 65 | _this2.activate_item = _this2.activate_item.bind(_this2); 66 | _this2.close_dropdown = _this2.close_dropdown.bind(_this2); 67 | _this2.toggle_dropdown = _this2.toggle_dropdown.bind(_this2); 68 | return _this2; 69 | } 70 | 71 | _createClass(Dropdown, [{ 72 | key: "close_dropdown", 73 | value: function close_dropdown() { 74 | this.setState(function (state) { 75 | return { active: false }; 76 | }); 77 | } 78 | }, { 79 | key: "toggle_dropdown", 80 | value: function toggle_dropdown() { 81 | if (this.state.active) { 82 | this.close_dropdown(); 83 | } else { 84 | this.activate_dropdown(); 85 | } 86 | } 87 | }, { 88 | key: "activate_dropdown", 89 | value: function activate_dropdown() { 90 | this.setState(function (state) { 91 | return { active: true }; 92 | }); 93 | } 94 | }, { 95 | key: "activate_item", 96 | value: function activate_item(i) { 97 | var _this3 = this; 98 | 99 | this.setState(function (state) { 100 | return { activity: i }; 101 | }); 102 | if (this.props.update_label) { 103 | //updates the label for the dropdown if this property is applied in JSX 104 | this.setState(function (state) { 105 | return { label_text: _this3.props.contents[i][0] }; 106 | }); 107 | } 108 | this.close_dropdown(); 109 | //if no default activity was selected, assumes that the item in the dropdown should not remain highlighted 110 | //This is because if props.def_active is not defined, it is assumed that the dropdown does not control 111 | //state and instead initiates an action 112 | if (this.props.def_active === undefined) { 113 | this.setState(function (state) { 114 | return { activity: -1 }; 115 | }); 116 | } 117 | } 118 | }, { 119 | key: "render", 120 | value: function render() { 121 | var _this4 = this; 122 | 123 | var a_list = []; 124 | var self = this; 125 | 126 | var _loop = function _loop(i) { 127 | var addition = ""; 128 | if (_this4.state.activity === i) { 129 | addition = " is-active"; 130 | } 131 | var selected_contents = _this4.props.contents[i]; 132 | a_list.push(React.createElement( 133 | "a", 134 | { value: _this4.props.contents[i], onClick: function onClick() { 135 | if (selected_contents.length > 1) { 136 | //this means that a function for onclick is provided 137 | selected_contents[1](); 138 | } 139 | self.activate_item(i); 140 | }, className: "dropdown-item" + addition, key: i }, 141 | selected_contents[0] 142 | )); 143 | }; 144 | 145 | for (var i = 0; i < this.props.contents.length; i++) { 146 | _loop(i); 147 | } 148 | var addition = ""; 149 | if (this.state.active) { 150 | addition = " is-active"; 151 | } 152 | return React.createElement( 153 | "div", 154 | { id: this.props.id, ref: this.setWrapperRef, className: "dropdown" + addition }, 155 | React.createElement( 156 | "div", 157 | { className: "dropdown-trigger", onClick: self.toggle_dropdown }, 158 | React.createElement( 159 | "button", 160 | { className: "button", "aria-haspopup": true, "aria-controls": "dropdown-menu" }, 161 | React.createElement( 162 | "span", 163 | null, 164 | React.createElement( 165 | "span", 166 | { className: "selected_name" }, 167 | this.state.label_text 168 | ), 169 | React.createElement( 170 | "span", 171 | { className: "icon is-small" }, 172 | React.createElement("i", { className: "fa fa-angle-down", "aria-hidden": "true" }) 173 | ) 174 | ) 175 | ) 176 | ), 177 | React.createElement( 178 | "div", 179 | { className: "dropdown-menu", role: "menu" }, 180 | React.createElement( 181 | "div", 182 | { className: "dropdown-content" }, 183 | a_list 184 | ) 185 | ) 186 | ); 187 | } 188 | }]); 189 | 190 | return Dropdown; 191 | }(OutClickable); 192 | 193 | //renders search type dropdown 194 | 195 | 196 | var domContainer_search = document.querySelector('#searchSelectContainer'); 197 | var angular_update = function angular_update(searchType) { 198 | var appElement = document.body; 199 | var $scope = angular.element(appElement).scope(); 200 | $scope.$apply(function () { 201 | $scope.searchType = searchType; 202 | $scope.searchChange(); 203 | }); 204 | }; 205 | 206 | //function that updates the angular function that listens for changes in search type 207 | var search_contents_list = [["Course ID", function () { 208 | angular_update("courseIDSearch"); 209 | }], ["Keywords", function () { 210 | angular_update("keywordSearch"); 211 | }], ["Instructor", function () { 212 | angular_update("instSearch"); 213 | }]]; 214 | //list of options for the dropdown 215 | ReactDOM.render(React.createElement(Dropdown, { id: "searchSelect", update_label: true, def_active: 0, def_text: "Search By", contents: search_contents_list }), domContainer_search); 216 | 217 | //renders schedule options dropdown 218 | var dom_container_schedule = document.querySelector("#scheduleOptionsContainer"); 219 | var new_schedule = function new_schedule() { 220 | angular.element(document.body).scope().sched.New(); 221 | }; 222 | var download_schedule = function download_schedule() { 223 | var $scope = angular.element(document.body).scope(); 224 | $scope.$apply(function () { 225 | $scope.sched.Download(); 226 | activate_modal(document.getElementById("schedule_modal")); 227 | }); 228 | //window.location = "#SchedModal"; 229 | }; 230 | 231 | var duplicate_schedule = function duplicate_schedule() { 232 | angular.element(document.body).scope().sched.Duplicate(); 233 | }; 234 | var rename_schedule = function rename_schedule() { 235 | angular.element(document.body).scope().sched.Rename(); 236 | }; 237 | var clear_schedule = function clear_schedule() { 238 | angular.element(document.body).scope().sched.Clear(); 239 | }; 240 | var delete_schedule = function delete_schedule() { 241 | angular.element(document.body).scope().sched.Delete(); 242 | }; 243 | var schedule_contents_list = [["New", new_schedule], ["Download", download_schedule], ["Duplicate", duplicate_schedule], ["Rename", rename_schedule], ["Clear", clear_schedule], ["Delete", delete_schedule]]; 244 | ReactDOM.render(React.createElement(Dropdown, { id: "scheduleDropdown", def_text: "Schedule Options", contents: schedule_contents_list }), dom_container_schedule); 245 | 246 | var ToggleButton = function (_OutClickable2) { 247 | _inherits(ToggleButton, _OutClickable2); 248 | 249 | //not a dropdown itself, but interacts with adjacent elements via css 250 | function ToggleButton(props) { 251 | _classCallCheck(this, ToggleButton); 252 | 253 | var _this5 = _possibleConstructorReturn(this, (ToggleButton.__proto__ || Object.getPrototypeOf(ToggleButton)).call(this, props)); 254 | 255 | _this5.props = props; 256 | _this5.containerHTML = props.parent.innerHTML; 257 | _this5.state = { active: false }; 258 | _this5.closeDropdown = _this5.closeDropdown.bind(_this5); 259 | _this5.activateDropdown = _this5.activateDropdown.bind(_this5); 260 | 261 | return _this5; 262 | } 263 | 264 | _createClass(ToggleButton, [{ 265 | key: "activateDropdown", 266 | value: function activateDropdown() { 267 | this.setState(function (state) { 268 | return { active: true }; 269 | }); 270 | } 271 | }, { 272 | key: "closeDropdown", 273 | value: function closeDropdown() { 274 | this.setState(function (state) { 275 | return { active: false }; 276 | }); 277 | } 278 | }, { 279 | key: "render", 280 | value: function render() { 281 | return React.createElement( 282 | Button, 283 | { ref: this.setWrapperRef, className: "toggle_button " + this.state.active }, 284 | this.props.name 285 | ); 286 | } 287 | }]); 288 | 289 | return ToggleButton; 290 | }(OutClickable); 291 | 292 | //const filter_search_dom_container = document.getElementById("FilterSearchButton"); 293 | 294 | //ReactDOM.render(,temp_dom_container); -------------------------------------------------------------------------------- /public/js/factories.js: -------------------------------------------------------------------------------- 1 | PCS.factory('PCR', function(){ 2 | return function PCR(data){ 3 | angular.forEach(data, function(item) { 4 | var qFrac = item.revs.cQ / 4; 5 | var dFrac = item.revs.cD / 4; 6 | var iFrac = item.revs.cI / 4; 7 | item.pcrQShade = Math.pow(qFrac, 3)*2; // This is the opacity of the PCR block 8 | item.pcrDShade = Math.pow(dFrac, 3)*2; 9 | item.pcrIShade = Math.pow(iFrac, 3)*2; 10 | if (qFrac < 0.50) {item.pcrQColor = 'black';} else {item.pcrQColor = 'white';} // It's hard to see white text on a light background 11 | if (dFrac < 0.50) {item.pcrDColor = 'black';} else {item.pcrDColor = 'white';} 12 | if (iFrac < 0.50) {item.pcrIColor = 'black';} else {item.pcrIColor = 'white';} 13 | item.revs.QDratio = item.revs.cQ - item.revs.cD; // This is my way of calculating if a class is "good and easy." R > 1 means good and easy, < 1 means bad and hard 14 | 15 | // Cleanup to keep incomplete data on the bottom; 16 | if (isNaN(item.revs.QDratio) || !isFinite(item.revs.QDratio)) {item.revs.QDratio = 0;} 17 | // the rating as a string - let's us make the actual rating something else and still show the correct number 18 | item.revs.cQT = item.revs.cQ.toFixed(2); 19 | if (item.revs.cQ === 0) {item.revs.cQT = '';} 20 | item.revs.cDT = item.revs.cD.toFixed(2); 21 | if (item.revs.cD === 0) {item.revs.cDT = ''; item.revs.QDratio = -100; item.revs.cD = 100;} 22 | }); 23 | return data; 24 | }; 25 | }); 26 | PCS.factory('UpdateCourseList', ['httpService', function(httpService){ 27 | var retObj = {}; 28 | retObj.getDeptCourses = function(dept, searchType, reqFilter, proFilter) { 29 | // Build the request URL 30 | var url = '/Search?searchType='+searchType+'&resultType=deptSearch&searchParam='+encodeURIComponent(dept); 31 | if (reqFilter) {url += '&reqParam='+reqFilter;} 32 | if (proFilter && proFilter !== 'noFilter') {url += '&proParam='+proFilter;} 33 | ga('send', 'event', 'Search', 'deptSearch', dept); 34 | return httpService.get(url).then(function(data) { 35 | return data; 36 | }, function(err) { 37 | if (!err.config.timeout.$$state.value) { 38 | ErrorAlert(err); // If there's an error, show an error dialog 39 | } else { 40 | return []; 41 | } 42 | }); 43 | }; 44 | return retObj; 45 | }]); 46 | PCS.factory('UpdateSectionList', ['httpService', function(httpService){ 47 | var retObj = {}; 48 | retObj.getCourseSections = function(course) { 49 | ga('send', 'event', 'Search', 'numbSearch', course); 50 | return httpService.get('/Search?searchType=courseIDSearch&resultType=numbSearch&searchParam='+course).then(function(data) { 51 | return data; 52 | }, function(err) { 53 | if (!err.config.timeout.$$state.value) { 54 | ErrorAlert(err); // If there's an error, show an error dialog 55 | } else { 56 | return []; 57 | } 58 | }); 59 | }; 60 | return retObj; 61 | }]); 62 | PCS.factory('UpdateSectionInfo', ['httpService', function(httpService){ 63 | var retObj = {}; 64 | retObj.getSectionInfo = function(section) { 65 | ga('send', 'event', 'Search', 'sectSearch', section); 66 | return httpService.get('/Search?searchType=courseIDSearch&resultType=sectSearch&searchParam='+section).then(function(data) { 67 | return data; 68 | }, function(err) { 69 | if (!err.config.timeout.$$state.value) { 70 | ErrorAlert(err); // If there's an error, show an error dialog 71 | } else { 72 | return {}; 73 | } 74 | }); 75 | }; 76 | return retObj; 77 | }]); 78 | PCS.factory('UpdateSchedules', ['httpService', function(httpService) { 79 | var retObj = {}; 80 | retObj.getSchedData = function(secID, needLoc) { 81 | var url = '/Sched?courseID='+secID; 82 | if (needLoc) {url += '&needLoc=1';} 83 | return httpService.get(url).then(function(data) { 84 | return data; 85 | }, function(err) { 86 | if (!err.config.timeout.$$state.value) { 87 | ErrorAlert(err); // If there's an error, show an error dialog 88 | } else { 89 | return {}; 90 | } 91 | }); 92 | }; 93 | return retObj; 94 | }]); 95 | // This service keeps track of pending requests 96 | PCS.service('pendingRequests', function() { 97 | var pending = []; 98 | this.get = function() { 99 | return pending; 100 | }; 101 | this.add = function(request) { 102 | pending.push(request); 103 | }; 104 | this.remove = function(request) { 105 | pending = pending.filter(function(p) { 106 | return p.url !== request; 107 | }); 108 | }; 109 | this.cancelAll = function() { 110 | angular.forEach(pending, function(p) { 111 | p.canceller.resolve('cancelled'); 112 | }); 113 | pending.length = 0; 114 | }; 115 | }); 116 | // This service wraps $http to make sure pending requests are tracked 117 | PCS.service('httpService', ['$http', '$q', 'pendingRequests', function($http, $q, pendingRequests) { 118 | this.get = function(url) { 119 | var canceller = $q.defer(); 120 | pendingRequests.add({ 121 | url: url, 122 | canceller: canceller 123 | }); 124 | //Request gets cancelled if the timeout-promise is resolved 125 | var requestPromise = $http.get(url, { timeout: canceller.promise }); 126 | //Once a request has failed or succeeded, remove it from the pending list 127 | requestPromise.finally(function() { 128 | pendingRequests.remove(url); 129 | }); 130 | return requestPromise; 131 | }; 132 | }]); -------------------------------------------------------------------------------- /public/js/functions.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('a[rel*=leanModal]').leanModal({ 3 | top: 70, 4 | closeButton: ".modal_close" 5 | }); // Define modal close button 6 | 7 | var subtitles = [ 8 | "Free shipping on all items in your course cart", 9 | "You can press the back button, but you don't even need to.", 10 | "Invented by Benjamin Franklin in 1793", 11 | "Faster than you can say 'Wawa run'", 12 | "Classes sine PennCourseSearch vanae", 13 | "On PennCourseSearch, no one knows you're Amy G.", 14 | "Designed by Ben in Speakman. Assembled in China.", 15 | "Help! I'm trapped in a NodeJS server! Bring Chipotle!", 16 | "With white sauce AND hot sauce", 17 | "Now 3.9% faster", 18 | "Number of squirrels online: 6", 19 | "Handling the business side since 2014", 20 | "Actually in touch" 21 | ]; 22 | var paymentNoteBase = "https://venmo.com/?txn=pay&recipients=BenBernstein&amount=1&share=f&audience=friends¬e="; 23 | var paymentNotes = [ 24 | "PennCourseSearch%20rocks%20my%20socks!", 25 | "Donation%20to%20PennInTouch%20Sucks,%20Inc.", 26 | "For%20your%20next%20trip%20to%20Wawa", 27 | "Offsetting%20the%20increased%20price%20of%20chicken%20over%20rice" 28 | ]; 29 | $('#subtitle').html(subtitles[Math.floor(Math.random() * subtitles.length)]); // Show a random subtitle 30 | $('#paymentNote').attr('href', paymentNoteBase + paymentNotes[Math.floor(Math.random() * paymentNotes.length)]); // Use a random payment note 31 | 32 | if (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { // Doesn't look good on mobile, so tell the user 33 | setTimeout(function() { 34 | sweetAlert({ 35 | title: 'PCS Alert', 36 | text: "Your device seems to be too small. PCS does not currently support mobile viewing, but we're looking to add it soon!", 37 | type: 'warning' 38 | }); 39 | }, 300); 40 | } else { 41 | $.get('/Status').done(function(statusMessage) { 42 | if (statusMessage !== 'hakol beseder') { // If there is a status message from the server, it will be passed in the #StatusMessage block 43 | setTimeout(function() { 44 | sweetAlert({ 45 | title: 'PCS Alert', 46 | html: true, 47 | text: statusMessage, 48 | type: 'warning' 49 | }); 50 | }, 300); 51 | console.log(statusMessage); 52 | } else { 53 | 54 | } 55 | }); 56 | } 57 | var today = new Date(); 58 | if (today.getMonth() === 3 && today.getDate() === 1) { 59 | $('.fa-volume-off').css("visibility", "visible"); 60 | $('body').append(''); 61 | } 62 | // GA Tracking 63 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 64 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 65 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 66 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 67 | 68 | ga('create', 'UA-49014722-4', 'auto'); 69 | ga('send', 'pageview'); 70 | 71 | if (!window.File) { 72 | $('#ImportButton').hide(); 73 | } 74 | }); 75 | 76 | function ErrorAlert(err) { 77 | // Shows an error dialog and logs the error to the console 78 | // Also includes the error report in an email that can be sent to Ben 79 | var errtext = 'An error occurred. Refresh or email Ben'; 80 | 81 | if (err.status === 512) { 82 | errtext = "PennInTouch just crapped out on us. Please try again."; 83 | } else if (err.status === 513) { 84 | errtext = "PennInTouch is refreshing, so we can't access class info :(
Please frustratedly wait about half an hour before trying again."; 85 | } 86 | sweetAlert({ 87 | title: '#awkward', 88 | html: true, 89 | text: errtext, 90 | type: 'error' 91 | }); 92 | } 93 | 94 | function Uniquify(str, arr) { 95 | // Given an array and a string, this ensures that the string doesn't already exist in the array 96 | if (arr.indexOf(str) === -1) { 97 | return str; 98 | } else { // If it does, then we "+1" the string 99 | var lastchar = str[str.length - 1]; 100 | if (isNaN(lastchar) || str[str.length - 2] !== ' ') { // e.g. if string == 'schedule' or 'ABC123' 101 | str += ' 2'; // becomes 'schedule 2' or 'ABC123 2' 102 | } else { // e.g. 'MEAM 101 2' 103 | str = str.slice(0, -2) + ' ' + (parseInt(lastchar) + 1); // becomes "MEAM 101 3" 104 | } 105 | return Uniquify(str, arr); // Make sure that this new name is unique 106 | } 107 | } 108 | 109 | var delay = (function() { 110 | var timer = 0; 111 | return function(callback, ms) { 112 | clearTimeout(timer); 113 | timer = setTimeout(callback, ms); 114 | }; 115 | })(); 116 | 117 | shuffle = function(v) { 118 | // Randomly reorders an array. 119 | //+ Jonas Raoni Soares Silva @ http://jsfromhell.com/array/shuffle [v1.0] 120 | for (var j, x, i = v.length; i; j = parseInt(Math.random() * i), x = v[--i], v[i] = v[j], v[j] = x); 121 | return v; 122 | }; 123 | 124 | function addrem(item, array) { 125 | // Adds or removes an item from an array depending on whether the array already contains that item. 126 | var index = array.indexOf(item); 127 | if (index === -1) { 128 | array.push(item); 129 | } else { 130 | array.splice(index, 1); 131 | } 132 | return array; 133 | } 134 | 135 | function FormatID(rawParam) { 136 | var searchParam = rawParam.replace(/\W/g, ''); // Replace non alpha-numeric characters 137 | var retArr = ['', '', '']; 138 | 139 | if (isFinite(searchParam[2])) { // If the third character is a number (e.g. BE100) 140 | splitTerms(2); 141 | } else if (isFinite(searchParam[3])) { // If the fourth character is a number (e.g. CIS110) 142 | splitTerms(3); 143 | } else if (isFinite(searchParam[4])) { // If the fifth character is a number (e.g. MEAM110) 144 | splitTerms(4); 145 | } else { 146 | retArr[0] = searchParam; 147 | } 148 | 149 | function splitTerms(n) { 150 | retArr[0] = searchParam.substr(0, n); 151 | retArr[1] = searchParam.substr(n, 3); 152 | retArr[2] = searchParam.substr(n+3, 3); 153 | } 154 | 155 | return retArr; 156 | } 157 | 158 | function Schedule(term) { 159 | // This is a blank schedule object constructor 160 | this.term = term; // e.g. "2016A" 161 | this.meetings = []; 162 | this.colorPalette = ["#e74c3c", "#f1c40f", "#3498db", "#9b59b6", "#e67e22", "#2ecc71", "#95a5a6", "#FF73FD", "#73F1FF", "#CA75FF", "#1abc9c", "#F64747", "#ecf0f1"]; // Standard colorPalette 163 | this.locAdded = false; 164 | } 165 | 166 | function GenMeetBlocks(sec) { 167 | var blocks = []; 168 | for (var day in sec.meetDay) { if (sec.meetDay.hasOwnProperty(day)) { 169 | var meetLetterDay = sec.meetDay[day]; // On which day does this meeting take place? 170 | var meetRoom = sec.meetLoc; 171 | var newid = sec.idDashed+'-'+meetLetterDay+sec.meetHour.toString().replace(".", ""); 172 | var asscsecs = sec.SchedAsscSecs; 173 | 174 | var newblock = { 175 | 'class': sec.idDashed, 176 | 'letterday': meetLetterDay, 177 | 'id': newid, 178 | 'startHr': sec.meetHour, 179 | 'duration': sec.hourLength, 180 | 'name': sec.idSpaced, 181 | 'room': meetRoom, 182 | 'asscsecs': asscsecs, 183 | "topc" : "blue" 184 | }; 185 | blocks.push(newblock); 186 | }} 187 | return blocks; 188 | } 189 | 190 | function TwoOverlap(block1, block2) { 191 | // Thank you to Stack Overflow user BC. for the function this is based on. 192 | // http://stackoverflow.com/questions/5419134/how-to-detect-if-two-divs-touch-with-jquery 193 | var y1 = (block1.startHr || block1.top); 194 | var h1 = (block1.duration || block1.height); 195 | var b1 = y1 + h1; 196 | 197 | var y2 = (block2.startHr || block2.top); 198 | var h2 = (block2.duration || block2.height); 199 | var b2 = y2 + h2; 200 | 201 | 202 | 203 | // This checks if the top of block 2 is lower down (higher value) than the bottom of block 1... 204 | // or if the top of block 1 is lower down (higher value) than the bottom of block 2. 205 | // In this case, they are not overlapping, so return false 206 | if (b1 <= (y2 + 0.0000001) || b2 <= (y1 + 0.0000001)) { 207 | return false; 208 | } else { 209 | return true; 210 | } 211 | } -------------------------------------------------------------------------------- /public/js/importSched.js: -------------------------------------------------------------------------------- 1 | function readCalFile() { 2 | var files = $('#schedInput')[0].files; 3 | if (!files || !files.length) {return;} 4 | 5 | var file = files[0]; 6 | var reader = new FileReader(); 7 | // If we use onloadend, we need to check the readyState. 8 | reader.onloadend = function(evt) { 9 | if (evt.target.readyState === FileReader.DONE) { // DONE == 2 10 | var secArr = parseCalFile(evt.target.result); 11 | if (secArr.length) { 12 | $('#importSubmit').prop('disabled', false); 13 | } else { 14 | $('#importSubmit').prop('disabled', true); 15 | } 16 | $('#secsToImport').empty(); 17 | for (var i = 0; i < secArr.length; i++) { 18 | $('#secsToImport').append(''+FormatID(secArr[i]).join(' ')+'
'); 19 | } 20 | return secArr; 21 | } else { 22 | $('#importSubmit').prop('disabled', true); 23 | return []; 24 | } 25 | }; 26 | var blob = file.slice(0, file.size-1); 27 | reader.readAsBinaryString(blob); 28 | } 29 | 30 | function parseCalFile(rawCal) { 31 | function FilterFunc (line) { 32 | if (line.split(':')[0] === 'SUMMARY') { 33 | return 1; 34 | } 35 | } 36 | function MapFunc (line) { 37 | return line.split('\r')[0].replace(/ /g, '').split(':')[1]; 38 | } 39 | 40 | var secs = rawCal.split('\n').filter(FilterFunc).map(MapFunc); 41 | var uniq = secs.filter(function(elem, pos) { 42 | return secs.indexOf(elem) === pos; 43 | }); 44 | return uniq; 45 | } -------------------------------------------------------------------------------- /public/js/modal_adjustments.js: -------------------------------------------------------------------------------- 1 | //adds outer click listener to all modals with ids 2 | arr(document.getElementsByClassName("modal")).forEach(function(el){ 3 | const id = el.getAttribute("id"); 4 | if(id!== undefined && id.length > 0){ 5 | add_outer_click_listener(["#"+id],function(){ 6 | close_modal(id); 7 | }, true); 8 | } 9 | }); -------------------------------------------------------------------------------- /public/js/plugins/angular-local-storage.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An Angular module that gives you access to the browsers local storage 3 | * @version v0.2.3 - 2015-10-11 4 | * @link https://github.com/grevory/angular-local-storage 5 | * @author grevory 6 | * @license MIT License, http://www.opensource.org/licenses/MIT 7 | */!function(a,b){"use strict";var c=b.isDefined,d=b.isUndefined,e=b.isNumber,f=b.isObject,g=b.isArray,h=b.extend,i=b.toJson,j=b.module("LocalStorageModule",[]);j.provider("localStorageService",function(){this.prefix="ls",this.storageType="localStorage",this.cookie={expiry:30,path:"/"},this.notify={setItem:!0,removeItem:!1},this.setPrefix=function(a){return this.prefix=a,this},this.setStorageType=function(a){return this.storageType=a,this},this.setStorageCookie=function(a,b){return this.cookie.expiry=a,this.cookie.path=b,this},this.setStorageCookieDomain=function(a){return this.cookie.domain=a,this},this.setNotify=function(a,b){return this.notify={setItem:a,removeItem:b},this},this.$get=["$rootScope","$window","$document","$parse",function(a,b,j,k){var l,m=this,n=m.prefix,o=m.cookie,p=m.notify,q=m.storageType;j?j[0]&&(j=j[0]):j=document,"."!==n.substr(-1)&&(n=n?n+".":"");var r=function(a){return n+a},s=function(){try{var c=q in b&&null!==b[q],d=r("__"+Math.round(1e7*Math.random()));return c&&(l=b[q],l.setItem(d,""),l.removeItem(d)),c}catch(e){return q="cookie",a.$broadcast("LocalStorageModule.notification.error",e.message),!1}}(),t=function(b,c){if(c=d(c)?null:i(c),!s||"cookie"===m.storageType)return s||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),p.setItem&&a.$broadcast("LocalStorageModule.notification.setitem",{key:b,newvalue:c,storageType:"cookie"}),z(b,c);try{l&&l.setItem(r(b),c),p.setItem&&a.$broadcast("LocalStorageModule.notification.setitem",{key:b,newvalue:c,storageType:m.storageType})}catch(e){return a.$broadcast("LocalStorageModule.notification.error",e.message),z(b,c)}return!0},u=function(b){if(!s||"cookie"===m.storageType)return s||a.$broadcast("LocalStorageModule.notification.warning","LOCAL_STORAGE_NOT_SUPPORTED"),A(b);var c=l?l.getItem(r(b)):null;if(!c||"null"===c)return null;try{return JSON.parse(c)}catch(d){return c}},v=function(){var b,c;for(b=0;b0||(j.cookie="test").indexOf.call(j.cookie,"test")>-1)}catch(c){return a.$broadcast("LocalStorageModule.notification.error",c.message),!1}}(),z=function(b,c,h){if(d(c))return!1;if((g(c)||f(c))&&(c=i(c)),!y)return a.$broadcast("LocalStorageModule.notification.error","COOKIES_NOT_SUPPORTED"),!1;try{var k="",l=new Date,m="";if(null===c?(l.setTime(l.getTime()+-864e5),k="; expires="+l.toGMTString(),c=""):e(h)&&0!==h?(l.setTime(l.getTime()+24*h*60*60*1e3),k="; expires="+l.toGMTString()):0!==o.expiry&&(l.setTime(l.getTime()+24*o.expiry*60*60*1e3),k="; expires="+l.toGMTString()),b){var n="; path="+o.path;o.domain&&(m="; domain="+o.domain),j.cookie=r(b)+"="+encodeURIComponent(c)+k+n+m}}catch(p){return a.$broadcast("LocalStorageModule.notification.error",p.message),!1}return!0},A=function(b){if(!y)return a.$broadcast("LocalStorageModule.notification.error","COOKIES_NOT_SUPPORTED"),!1;for(var c=j.cookie&&j.cookie.split(";")||[],d=0;d").addClass("angular-tooltip angular-tooltip-"+o).html(i.title||i.tooltip);angular.element(document).find("body").append(l);var r=e.calculatePosition(l,o);l.css(r),l.addClass("angular-tooltip-fade-in")}),e.removeTooltip=function(){var e=angular.element(document.querySelectorAll(".angular-tooltip"));e.removeClass("angular-tooltip-fade-in"),t(function(){e.remove()},300)},e.getDirection=function(){return o.attr("tooltip-direction")||o.attr("title-direction")||"top"},e.calculatePosition=function(t,e){var i=t[0].getBoundingClientRect(),l=o[0].getBoundingClientRect(),r=window.scrollX||document.documentElement.scrollLeft,n=window.scrollY||document.documentElement.scrollTop,p=12;switch(e){case"top":case"top-center":case"top-middle":return{left:l.left+l.width/2-i.width/2+r+"px",top:l.top-i.height-p/2+n+"px"};case"top-right":return{left:l.left+l.width-p+r+"px",top:l.top-i.height-p/2+n+"px"};case"right-top":return{left:l.left+l.width+p/2+r+"px",top:l.top-i.height+p+n+"px"};case"right":case"right-center":case"right-middle":return{left:l.left+l.width+p/2+r+"px",top:l.top+l.height/2-i.height/2+n+"px"};case"right-bottom":return{left:l.left+l.width+p/2+r+"px",top:l.top+l.height-p+n+"px"};case"bottom-right":return{left:l.left+l.width-p+r+"px",top:l.top+l.height+p/2+n+"px"};case"bottom":case"bottom-center":case"bottom-middle":return{left:l.left+l.width/2-i.width/2+r+"px",top:l.top+l.height+p/2+n+"px"};case"bottom-left":return{left:l.left-i.width+p+r+"px",top:l.top+l.height+p/2+n+"px"};case"left-bottom":return{left:l.left-i.width-p/2+r+"px",top:l.top+l.height-p+n+"px"};case"left":case"left-center":case"left-middle":return{left:l.left-i.width-p/2+r+"px",top:l.top+l.height/2-i.height/2+n+"px"};case"left-top":return{left:l.left-i.width-p/2+r+"px",top:l.top-i.height+p+n+"px"};case"top-left":return{left:l.left-i.width+p+r+"px",top:l.top-i.height-p/2+n+"px"}}},o.on("mouseout",e.removeTooltip),o.on("destroy",e.removeTooltip),e.$on("$destroy",e.removeTooltip))}}};t.$inject=["$timeout"],angular.module("tooltips",[]).directive("title",t).directive("tooltip",t)}(); -------------------------------------------------------------------------------- /public/js/plugins/jquery.leanModal.min.js: -------------------------------------------------------------------------------- 1 | // leanModal v1.1 by Ray Stone - http://finelysliced.com.au 2 | // Dual licensed under the MIT and GPL 3 | !function(e){e.fn.extend({leanModal:function(n){function o(n){e("#lean_overlay").fadeOut(200),e(n).css({display:"none"})}var t={top:100,overlay:.5,closeButton:null},a=e("
");return e("body").append(a),n=e.extend(t,n),this.each(function(){var t=n;e(this).click(function(n){var a=e(this).attr("href");e("#lean_overlay").click(function(){o(a)}),e(t.closeButton).click(function(){o(a)});var l=(e(a).outerHeight(),e(a).outerWidth());e("#lean_overlay").css({display:"block",opacity:0}),e("#lean_overlay").fadeTo(200,t.overlay),e(a).css({display:"block",position:"fixed",opacity:0,"z-index":11e3,left:"50%","margin-left":-(l/2)+"px",top:t.top+"px"}),e(a).fadeTo(200,1),n.preventDefault()})})}})}(jQuery); -------------------------------------------------------------------------------- /public/js/ui_adjustment.js: -------------------------------------------------------------------------------- 1 | //file for managing color scheme and other aspects of UI 2 | 3 | //possible color classes (corresponds with CSS classes) 4 | const top_colors_recitation_save = ["red", "orange", "pink"]; 5 | const top_colors_other_save = ["blue", "aqua", "green", "sea", "indigo"]; 6 | 7 | //available color classes 8 | let top_colors_recitation = []; 9 | let top_colors_other = []; 10 | 11 | //makes all recitation colors available 12 | const reset_recitation_colors = function(){ 13 | top_colors_recitation = top_colors_recitation_save.slice(); 14 | }; 15 | 16 | //makes all other colors available 17 | const reset_other_colors = function(){ 18 | top_colors_other = top_colors_other_save.slice(); 19 | }; 20 | 21 | //dictionary associating class name with color 22 | let class_colors = {}; 23 | 24 | //makes all colors available 25 | const reset_colors = function (){ 26 | reset_recitation_colors(); 27 | reset_other_colors(); 28 | class_colors = {}; 29 | }; 30 | 31 | //generates a color from a given day of the week, hour, and course name 32 | const generate_color = function (day, hour, name) { 33 | var temp_color = class_colors[name]; 34 | if(temp_color !== undefined){ 35 | return temp_color; 36 | }else { 37 | let chosen_list = null; 38 | if (parseInt(name.substring(name.length - 3, name.length)) >= 100) { 39 | chosen_list = top_colors_recitation; 40 | if (chosen_list.length === 0) { 41 | reset_recitation_colors(); 42 | chosen_list = top_colors_recitation; 43 | } 44 | } else { 45 | chosen_list = top_colors_other; 46 | if (chosen_list.length === 0) { 47 | reset_other_colors(); 48 | chosen_list = top_colors_other; 49 | } 50 | } 51 | const index = (["M", "T", "W", "H", "F"].indexOf(day) % 2 + Math.round(hour * 2)) % chosen_list.length; 52 | const result = chosen_list[index]; 53 | chosen_list.splice(index, 1); 54 | class_colors[name] = result; 55 | return result; 56 | } 57 | }; 58 | 59 | //returns whether child is a child of parent 60 | //credit to https://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-contained-within-another 61 | const is_descendant = function(parent, child) { 62 | var node = child.parentNode; 63 | while (node != null) { 64 | if (node == parent) { 65 | return true; 66 | } 67 | node = node.parentNode; 68 | } 69 | return false; 70 | }; 71 | 72 | //deactivates a bulma dropdown/dropdown item 73 | const deactivate_node = function(node){ 74 | let prev_class = node.getAttribute("class"); 75 | node.setAttribute("class",prev_class.replace("is-active","").replace("selected","")); 76 | }; 77 | 78 | //activates a bulma dropdown/dropdown item 79 | const activate_node = function(node){ 80 | let prev_class = node.getAttribute("class"); 81 | if(prev_class.indexOf("item")!==-1){ 82 | node.setAttribute("class",prev_class + " selected is-active"); 83 | }else{ 84 | node.setAttribute("class",prev_class + " is-active"); 85 | } 86 | 87 | }; 88 | 89 | //toggles activation of a bulma dropdown 90 | const toggle_activation = function(dropdown){ 91 | let prev_class = dropdown.getAttribute("class"); 92 | if(prev_class.indexOf("is-active")!==-1){ 93 | deactivate_node(dropdown); 94 | }else{ 95 | activate_node(dropdown); 96 | window.addEventListener("click",function(e) { 97 | if(!(e.target == dropdown || is_descendant(dropdown,e.target))){ 98 | deactivate_node(dropdown); 99 | } 100 | }); 101 | } 102 | }; 103 | 104 | //returns the parent node dropdown of the given node 105 | function find_parent_dropdown(node){ 106 | if(node.parentNode !== undefined && node.parentNode.getAttribute("class").indexOf("dropdown")!==-1){ 107 | return find_parent_dropdown(node.parentNode); 108 | }else{ 109 | return node; 110 | } 111 | } 112 | 113 | //takes in an HTMLCollection and returns an array 114 | function arr(elementsByClassName) { 115 | result = []; 116 | for(var i = 0;i 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /react_components/dropdown.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class OutClickable extends React.Component{ 4 | // a component that you can "click out" of 5 | //requires that ref={this.setWrapperRef} is added as an attribute 6 | constructor(props){ 7 | super(props); 8 | this.setWrapperRef = this.setWrapperRef.bind(this); 9 | this.handleClickOutside = this.handleClickOutside.bind(this); 10 | document.addEventListener("click", this.handleClickOutside); 11 | } 12 | 13 | /** 14 | * Alert if clicked on outside of element 15 | */ 16 | handleClickOutside(event) { 17 | if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { 18 | this.close_dropdown(); 19 | } 20 | } 21 | 22 | setWrapperRef(node) { 23 | this.wrapperRef = node; 24 | } 25 | 26 | } 27 | 28 | class Dropdown extends OutClickable{ 29 | constructor(props) { 30 | super(props); 31 | let starting_activity = -1; 32 | //if props.def_active is not defined, it is assumed that the dropdown does not control 33 | //state and instead initiates an action 34 | if(props.def_active !== undefined){ 35 | starting_activity = props.def_active; 36 | } 37 | this.state = {active: false, activity: starting_activity, label_text: props.def_text}; 38 | this.activate_dropdown = this.activate_dropdown.bind(this); 39 | this.activate_item = this.activate_item.bind(this); 40 | this.close_dropdown = this.close_dropdown.bind(this); 41 | this.toggle_dropdown = this.toggle_dropdown.bind(this); 42 | } 43 | 44 | close_dropdown() { 45 | this.setState(state => ({active: false})); 46 | } 47 | 48 | toggle_dropdown(){ 49 | if(this.state.active){ 50 | this.close_dropdown(); 51 | }else{ 52 | this.activate_dropdown(); 53 | } 54 | } 55 | 56 | activate_dropdown() { 57 | this.setState(state => ({active: true})); 58 | } 59 | 60 | activate_item(i) { 61 | this.setState(state => ({activity: i})); 62 | if(this.props.update_label){ 63 | //updates the label for the dropdown if this property is applied in JSX 64 | this.setState(state=>({label_text: this.props.contents[i][0]})); 65 | } 66 | this.close_dropdown(); 67 | //if no default activity was selected, assumes that the item in the dropdown should not remain highlighted 68 | //This is because if props.def_active is not defined, it is assumed that the dropdown does not control 69 | //state and instead initiates an action 70 | if(this.props.def_active === undefined){ 71 | this.setState(state => ({activity: -1})); 72 | } 73 | } 74 | 75 | render() { 76 | const a_list = []; 77 | let self = this; 78 | for (let i = 0; i < this.props.contents.length; i++) { 79 | let addition = ""; 80 | if (this.state.activity === i) { 81 | addition = " is-active"; 82 | } 83 | const selected_contents = this.props.contents[i]; 84 | a_list.push( 1){ 86 | //this means that a function for onclick is provided 87 | selected_contents[1](); 88 | } 89 | self.activate_item(i) 90 | }} className={"dropdown-item" + addition} key={i}>{selected_contents[0]}); 91 | } 92 | let addition = ""; 93 | if (this.state.active) { 94 | addition = " is-active"; 95 | } 96 | return ( 97 |
98 |
99 |