├── .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 | [](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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
107 |
108 |
109 |
110 | {a_list}
111 |
112 |
113 |
114 | )
115 | }
116 | }
117 |
118 | //renders search type dropdown
119 | const domContainer_search = document.querySelector('#searchSelectContainer');
120 | const angular_update = function(searchType){
121 | var appElement = document.body;
122 | var $scope = angular.element(appElement).scope();
123 | $scope.$apply(function(){
124 | $scope.searchType = searchType;
125 | $scope.searchChange();
126 | });
127 | };
128 |
129 |
130 | //function that updates the angular function that listens for changes in search type
131 | const search_contents_list = [["Course ID",function(){angular_update("courseIDSearch")}],
132 | ["Keywords",function(){angular_update("keywordSearch")}],
133 | ["Instructor",function(){angular_update("instSearch")}]];
134 | //list of options for the dropdown
135 | ReactDOM.render(, domContainer_search);
136 |
137 | //renders schedule options dropdown
138 | const dom_container_schedule = document.querySelector("#scheduleOptionsContainer");
139 | const new_schedule = function(){angular.element(document.body).scope().sched.New()};
140 | const download_schedule = function(){
141 | var $scope = angular.element(document.body).scope();
142 | $scope.$apply(function(){
143 | $scope.sched.Download();
144 | activate_modal(document.getElementById("schedule_modal"));
145 | });
146 | //window.location = "#SchedModal";
147 | };
148 |
149 | const duplicate_schedule = function(){angular.element(document.body).scope().sched.Duplicate()};
150 | const rename_schedule = function(){angular.element(document.body).scope().sched.Rename()};
151 | const clear_schedule = function(){angular.element(document.body).scope().sched.Clear()};
152 | const delete_schedule = function(){angular.element(document.body).scope().sched.Delete()};
153 | const schedule_contents_list = [["New",new_schedule],
154 | ["Download",download_schedule],
155 | ["Duplicate",duplicate_schedule],
156 | ["Rename",rename_schedule],
157 | ["Clear",clear_schedule],
158 | ["Delete",delete_schedule]
159 | ];
160 | ReactDOM.render(, dom_container_schedule);
161 |
162 | class ToggleButton extends OutClickable{
163 | //not a dropdown itself, but interacts with adjacent elements via css
164 | constructor(props){
165 | super(props);
166 | this.props = props;
167 | this.containerHTML = props.parent.innerHTML;
168 | this.state = {active: false};
169 | this.closeDropdown = this.closeDropdown.bind(this);
170 | this.activateDropdown = this.activateDropdown.bind(this);
171 |
172 | }
173 |
174 | activateDropdown(){
175 | this.setState(state=>({active:true}));
176 | }
177 |
178 | closeDropdown(){
179 | this.setState(state=>({active:false}));
180 | }
181 |
182 | render(){
183 | return ;
184 | }
185 |
186 | }
187 |
188 | //const filter_search_dom_container = document.getElementById("FilterSearchButton");
189 |
190 | //ReactDOM.render(,temp_dom_container);
--------------------------------------------------------------------------------
/reqFunctions.js:
--------------------------------------------------------------------------------
1 | var requirements = {};
2 |
3 | var WhartonReq = require('./DB/wharreq.json');
4 | var EngineerReq = require('./DB/engreq.json');
5 |
6 | var collegeCodes = {
7 | Society: "MDS",
8 | History: "MDH",
9 | Arts: "MDA",
10 | Humanities: "MDO",
11 | Living: "MDL",
12 | Physical: "MDP",
13 | Natural: "MDN",
14 | Writing: "MWC",
15 | College: "MQS",
16 | Formal: "MFR",
17 | Cross: "MC1",
18 | Cultural: "MC2"
19 | };
20 |
21 | // The requirement code -> name map
22 | var reqCodes = {
23 | MDS: "Society Sector",
24 | MDH: "History & Tradition Sector",
25 | MDA: "Arts & Letters Sector",
26 | MDO: "Humanities & Social Science Sector",
27 | MDL: "Living World Sector",
28 | MDP: "Physical World Sector",
29 | MDN: "Natural Science & Math Sector",
30 | MWC: "Writing Requirement",
31 | MQS: "College Quantitative Data Analysis Req.",
32 | MFR: "Formal Reasoning Course",
33 | MC1: "Cross Cultural Analysis",
34 | MC2: "Cultural Diversity in the US",
35 | WGLO: "Wharton - Global Environment",
36 | WSST: "Wharton - Social Structures",
37 | WSAT: "Wharton - Science and Technology",
38 | WLAC: "Wharton - Language, Arts & Culture",
39 | WNHR: "Wharton 2017 - Humanities",
40 | WNNS: "Wharton 2017 - Natural Science",
41 | WNSS: "Wharton 2017 - Social Structures",
42 | WNFR: "Wharton 2017 - Flexible",
43 | WURE: "Wharton 2017 - Unrestricted Elective",
44 | WNSA: "Wharton 2017 - See Advisor",
45 | WCCY: "Wharton 2017 - Cross-Cultural Perspectives",
46 | WCCS: "Wharton 2017 - CCP See Advisor",
47 | WCCC: "Wharton 2017 - CCP CDUS",
48 | EMAT: "SEAS - Math",
49 | ESCI: "SEAS - Natural Science",
50 | EENG: "SEAS - Engineering",
51 | ESSC: "SEAS - Social Sciences",
52 | EHUM: "SEAS - Humanities",
53 | ETBS: "SEAS - Technology, Business, and Society",
54 | EWRT: "SEAS - Writing",
55 | ENOC: "SEAS - No Credit"
56 | };
57 |
58 | requirements.GetRequirements = function(section) {
59 | var idDashed = (section.course_department + '-' + section.course_number);
60 |
61 | var reqList = section.fulfills_college_requirements; // Pull standard college requirements
62 | var reqCodesList = [];
63 | if (reqList[0]) {
64 | reqCodesList[0] = collegeCodes[reqList[0].split(" ")[0]]; // Generate the req codes
65 | }
66 | if (reqList[1]) {
67 | reqCodesList[1] = collegeCodes[reqList[1].split(" ")[0]];
68 | }
69 |
70 | var extraReq = section.important_notes; // Sometimes there are extra college requirements cause why not
71 | var extraReqCode;
72 | for (var i = 0; i < extraReq.length; i++) { // Run through each one
73 | extraReqCode = collegeCodes[extraReq[i].split(" ")[0]];
74 | if (extraReqCode === 'MDO' || extraReqCode === 'MDN') { // If it matches humanities or natural science
75 | // reqList.push(extraReq[i]);
76 | reqCodesList.push(extraReqCode);
77 | } else if (section.requirements[0]) {
78 | if (section.requirements[0].registration_control_code === 'MDB') { // Both?
79 | reqCodesList.push('MDO');
80 | reqCodesList.push('MDN');
81 | }
82 | }
83 | if (extraReq[i].split(" ")[0] !== "Registration") { // Other notes that are not about "registration for associated"
84 | reqList.push(extraReq[i]);
85 | }
86 | }
87 |
88 | if (WhartonReq[idDashed]) {
89 | var rules = Object.keys(WhartonReq[idDashed]);
90 | for (var r in rules) {
91 | if (WhartonReq[idDashed][rules[r]]) {
92 | var thisval = WhartonReq[idDashed][rules[r]]; // Pull the course's wharton requirement if it has one
93 | reqCodesList.push(thisval);
94 | reqList.push(reqCodes[thisval]);
95 | }
96 | }
97 | }
98 |
99 | engReturn = EngReqRules(section.course_department, section.course_number, section.crosslistings[0]);
100 | reqCodesList = reqCodesList.concat(engReturn[0]);
101 | reqList = reqList.concat(engReturn[1]);
102 | return [reqCodesList, reqList];
103 | };
104 |
105 | function EngReqRules(dept, num, cross) {
106 | var engreqCodesList = [];
107 | var engreqList = [];
108 | var thisEngObj = {};
109 | // Get the departmental rules first
110 | if (EngineerReq[dept]) {
111 | // Check math
112 | if (EngineerReq[dept].math) {
113 | if (dept !== 'MATH' || (dept === 'MATH' && Number(num) >= 104)) { // Only math classes >= 104 count
114 | thisEngObj.math = true;
115 | }
116 | }
117 | // Check natsci
118 | if (EngineerReq[dept].natsci) {
119 | if ((['BIOL', 'GEOL', 'PHYS'].indexOf(dept) < 0) || // No restrictions on other departments
120 | (dept === 'BIOL' && Number(num) > 100) || // Only Biol classes > 100
121 | (dept === 'GEOL' && Number(num) > 200) || // Only Geol classes > 200
122 | (dept === 'PHYS' && Number(num) >=150)) { // Only Phys classes >=150
123 | thisEngObj.natsci = true;
124 | }
125 | }
126 | // Check Engineering
127 | if (EngineerReq[dept].eng) {
128 | if (num !== '296' && num !== '297' && num < 600) { // No engineering classes with num 296 or 297 and not necessarily 600 level
129 | thisEngObj.eng = true;
130 | }
131 | }
132 | if (EngineerReq[dept].ss) {
133 | if (Number(num) < 500) { // No 500-level ss classes count
134 | thisEngObj.ss = true;
135 | }
136 | }
137 | if (EngineerReq[dept].hum) {
138 | if (Number(num) < 500) { // No 500-level ss classes count
139 | thisEngObj.hum = true;
140 | }
141 | }
142 | if (EngineerReq[dept].tbs) {
143 | thisEngObj.tbs = true;
144 | }
145 | if (EngineerReq[dept].nocred) {
146 | if ((dept === 'PHYS' && Number(num) < 140) || (dept === 'STAT' && Number(num) < 430)) {
147 | thisEngObj.nocred = true;
148 | }
149 | }
150 | }
151 | specificReq = (EngineerReq[dept+'-'+num] || {});
152 | thisEngObj = Object.assign(thisEngObj, specificReq);
153 |
154 | // tbs classes don't count for engineering
155 | if (thisEngObj.tbs) {thisEngObj.eng = false;}
156 |
157 | if (dept === 'IPD' && cross) { // IPD Rule
158 | if (cross.subject === 'ARCH' || cross.subject === 'EAS' || cross.subject === 'FNAR') {
159 | thisEngObj.eng = false;
160 | }
161 | }
162 |
163 | if (thisEngObj.math) {engreqCodesList.push('EMAT'); engreqList.push(reqCodes.EMAT);}
164 | if (thisEngObj.natsci) {engreqCodesList.push('ESCI'); engreqList.push(reqCodes.ESCI);}
165 | if (thisEngObj.eng) {engreqCodesList.push('EENG'); engreqList.push(reqCodes.EENG);}
166 | if (thisEngObj.ss) {engreqCodesList.push('ESSC'); engreqList.push(reqCodes.ESSC);}
167 | if (thisEngObj.hum) {engreqCodesList.push('EHUM'); engreqList.push(reqCodes.EHUM);}
168 | if (thisEngObj.tbs) {engreqCodesList.push('ETBS'); engreqList.push(reqCodes.ETBS);}
169 | if (thisEngObj.writ) {engreqCodesList.push('EWRT'); engreqList.push(reqCodes.EWRT);}
170 | if (thisEngObj.nocred) {engreqCodesList.push('ENOC'); engreqList.push(reqCodes.ENOC);}
171 |
172 | var listCross = ['AFST', 'ASAM', 'AFRC', 'AAMW', 'CINE', 'GSWS'];
173 |
174 | if (listCross.indexOf(dept) > -1 && cross) {
175 | engReturn = EngReqRules(cross.subject, cross.course_id);
176 | engreqCodesList = engReturn[0];
177 | engreqList = engReturn[1];
178 | }
179 |
180 | return [engreqCodesList, engreqList];
181 | }
182 |
183 | module.exports = requirements;
--------------------------------------------------------------------------------
/ui_adjustment.js:
--------------------------------------------------------------------------------
1 | //file for managing color scheme and other aspects of UI
2 |
3 | //possible color classes (corresponds with CSS classes)
4 | var top_colors_recitation_save = ["red", "orange", "pink"];
5 | var top_colors_other_save = ["blue", "aqua", "green", "sea", "indigo"];
6 |
7 | //available color classes
8 | var top_colors_recitation = [];
9 | var top_colors_other = [];
10 |
11 | //makes all recitation colors available
12 | var reset_recitation_colors = function reset_recitation_colors() {
13 | top_colors_recitation = top_colors_recitation_save.slice();
14 | };
15 |
16 | //makes all other colors available
17 | var reset_other_colors = function reset_other_colors() {
18 | top_colors_other = top_colors_other_save.slice();
19 | };
20 |
21 | //dictionary associating class name with color
22 | var class_colors = {};
23 |
24 | //makes all colors available
25 | var reset_colors = function reset_colors() {
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 | var generate_color = function generate_color(day, hour, name) {
33 | var temp_color = class_colors[name];
34 | if (temp_color !== undefined) {
35 | return temp_color;
36 | } else {
37 | var 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 | var index = (["M", "T", "W", "H", "F"].indexOf(day) % 2 + Math.round(hour * 2)) % chosen_list.length;
52 | var _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 | var is_descendant = function is_descendant(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 | var deactivate_node = function deactivate_node(node) {
74 | var 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 | var activate_node = function activate_node(node) {
80 | var 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 | //toggles activation of a bulma dropdown
89 | var toggle_activation = function toggle_activation(dropdown) {
90 | var prev_class = dropdown.getAttribute("class");
91 | if (prev_class.indexOf("is-active") !== -1) {
92 | deactivate_node(dropdown);
93 | } else {
94 | activate_node(dropdown);
95 | window.addEventListener("click", function (e) {
96 | if (!(e.target == dropdown || is_descendant(dropdown, e.target))) {
97 | deactivate_node(dropdown);
98 | }
99 | });
100 | }
101 | };
102 |
103 | //returns the parent node dropdown of the given node
104 | function find_parent_dropdown(node) {
105 | if (node.parentNode !== undefined && node.parentNode.getAttribute("class").indexOf("dropdown") !== -1) {
106 | return find_parent_dropdown(node.parentNode);
107 | } else {
108 | return node;
109 | }
110 | }
111 |
112 | //takes in an HTMLCollection and returns an array
113 | function arr(elementsByClassName) {
114 | result = [];
115 | for (var i = 0; i < elementsByClassName.length; i++) {
116 | result[i] = elementsByClassName[i];
117 | }
118 | return result;
119 | }
120 |
121 | //activates bulma dropdown item
122 | var activate_dropdown_item = function activate_dropdown_item(dropdown_item) {
123 | var prev_class = dropdown_item.getAttribute("class");
124 | if (prev_class.indexOf("is-active") === -1) {
125 | arr(document.getElementsByClassName("dropdown-item")).forEach(function (node) {
126 | if (node.getAttribute("class").indexOf("item") !== -1) {
127 | deactivate_node(node);
128 | }
129 | });
130 | activate_node(dropdown_item);
131 | var parent_node = find_parent_dropdown(dropdown_item);
132 | //let text_node = parent_node.childNodes[0];
133 | //console.log(text_node);
134 | var new_text = dropdown_item.textContent;
135 | parent_node.setAttribute("value", new_text.replace(" ", "").replace("\n", "").replace("\t", ""));
136 | angular.element(parent_node).scope().searchChange();
137 | parent_node.childNodes[1].childNodes[1].childNodes[1].childNodes[0].textContent = new_text;
138 | }
139 | };
--------------------------------------------------------------------------------
/views/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PennCourseSearch - 404
5 |
6 |
7 | Where do you think you're going?
8 |
9 |
10 |
--------------------------------------------------------------------------------
/views/50x.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PennCourseSearch - Error
5 |
6 |
7 | The server is currently being MERT'ed right now.
8 | Don't worry! Ben is on it. Please check back soon.
9 | Let me in!
10 |
11 |
--------------------------------------------------------------------------------