├── .travis.yml ├── src ├── history │ ├── package.json │ ├── chunk.js │ ├── index.js │ ├── scrape.js │ ├── condense.js │ └── history.test.js ├── gradebook │ ├── package.json │ ├── index.js │ ├── scrape.js │ ├── gradebook.test.js │ ├── parse.js │ └── data │ │ └── payload.data.js ├── reportcard │ ├── package.json │ ├── index.js │ ├── parse.js │ ├── scrape.js │ ├── condense.js │ └── reportcard.test.js └── authenticate │ ├── package.json │ ├── index.js │ ├── decode.js │ ├── login.js │ └── authenticate.test.js ├── .eslintrc.json ├── CHANGELOG.md ├── package.json ├── index.js ├── LICENSE ├── .gitignore ├── index.test.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '7.8.0' -------------------------------------------------------------------------------- /src/history/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "history", 3 | "main": "index.js" 4 | } -------------------------------------------------------------------------------- /src/gradebook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gradebook", 3 | "main": "index.js" 4 | } -------------------------------------------------------------------------------- /src/reportcard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reportcard", 3 | "main": "index.js" 4 | } -------------------------------------------------------------------------------- /src/authenticate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authenticate", 3 | "main": "index.js" 4 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "linebreak-style": 0, 5 | "import/no-extraneous-dependencies": false 6 | } 7 | } -------------------------------------------------------------------------------- /src/history/chunk.js: -------------------------------------------------------------------------------- 1 | module.exports = chunkFunc => (arr, yearData) => [ 2 | ...arr, 3 | ...yearData.reduce((years, row) => (chunkFunc(row) 4 | ? [...years, [row]] 5 | : [...years.slice(0, -1), [...years.slice(-1)[0], row]]), []), 6 | ]; 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.1 (5/6/19) 2 | 3 | #### Bug Fixes 4 | 5 | * fixed `.scrapeReport` returning empty courses 6 | 7 | # 1.0.0 (3/17/19) 8 | 9 | #### Features 10 | 11 | * added `scraper` 12 | * added `.scrapeGradebook` 13 | * added `.scrapeHistory` 14 | * added `.scrapeReport` -------------------------------------------------------------------------------- /src/authenticate/index.js: -------------------------------------------------------------------------------- 1 | 2 | const axios = require('axios'); 3 | const decode = require('./decode'); 4 | const login = require('./login'); 5 | 6 | /* expose a more friendly api */ 7 | module.exports = skywardURL => ( 8 | (user, pass) => login(axios, skywardURL)({ user, pass }) 9 | .then(decode) 10 | ); 11 | -------------------------------------------------------------------------------- /src/gradebook/index.js: -------------------------------------------------------------------------------- 1 | 2 | const axios = require('axios'); 3 | const scrape = require('./scrape'); 4 | const parse = require('./parse'); 5 | 6 | module.exports = { 7 | fetch: skywardURL => ( 8 | auth => ( 9 | (course, bucket) => scrape(axios, skywardURL)(auth, course, bucket) 10 | ) 11 | ), 12 | 13 | getData: parse, 14 | }; 15 | -------------------------------------------------------------------------------- /src/history/index.js: -------------------------------------------------------------------------------- 1 | 2 | const axios = require('axios'); 3 | const scrape = require('./scrape'); 4 | const parse = require('../reportcard/parse'); 5 | const condense = require('./condense'); 6 | 7 | module.exports = { 8 | fetch: skywardURL => ( 9 | auth => scrape(axios, skywardURL)(auth) 10 | ), 11 | 12 | getData: raw => condense(parse(raw)), 13 | }; 14 | -------------------------------------------------------------------------------- /src/reportcard/index.js: -------------------------------------------------------------------------------- 1 | 2 | const axios = require('axios'); 3 | const scrape = require('./scrape'); 4 | const parse = require('./parse'); 5 | const condense = require('./condense'); 6 | 7 | /* expose a more friendly api */ 8 | module.exports = { 9 | fetch: skywardURL => ( 10 | auth => scrape(axios, skywardURL)(auth) 11 | ), 12 | 13 | getData: raw => condense(parse(raw)), 14 | }; 15 | -------------------------------------------------------------------------------- /src/reportcard/parse.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | 3 | module.exports = ({ data }) => { 4 | const $ = cheerio.load(data); 5 | 6 | const script = $('script[data-rel="sff"]').html(); 7 | 8 | const results = /\$\.extend\(\(sff\.getValue\('sf_gridObjects'\) \|\| {}\), ([\s\S]*)\)\);/g.exec(script); 9 | 10 | return (results === null) ? {} : eval(`0 || ${results[1]}`); // eslint doesn't like `eval`, and neither do I 11 | }; 12 | -------------------------------------------------------------------------------- /src/authenticate/decode.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ data } = {}) => { 2 | if (!data) throw new TypeError('data is required'); 3 | 4 | if (data === '
  • Invalid login or password.
  • ') throw new Error('Invalid Skyward credentials'); 5 | 6 | const tokens = data.slice(4, -5) 7 | .split('^'); 8 | 9 | if (tokens.length < 15) throw new Error('Malformed auth data'); 10 | 11 | return { 12 | dwd: tokens[0], wfaacl: tokens[3], encses: tokens[14], sessionId: `${tokens[1]}%15${tokens[2]}`, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/reportcard/scrape.js: -------------------------------------------------------------------------------- 1 | 2 | const body = ({ dwd, wfaacl, encses }) => { 3 | if (!dwd || !wfaacl || !encses) throw new TypeError('dwd, wfaacl, & encses are required'); 4 | 5 | return `dwd=${dwd}&wfaacl=${wfaacl}&encses=${encses}`; 6 | }; 7 | 8 | module.exports = (axios, skywardURL) => (auth) => { 9 | if (!axios || !skywardURL) throw new TypeError('axios & skywardURL are required'); 10 | 11 | return axios({ 12 | url: '../sfgradebook001.w', 13 | baseURL: skywardURL, 14 | method: 'post', 15 | data: body(auth), 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/authenticate/login.js: -------------------------------------------------------------------------------- 1 | 2 | const body = ({ user, pass }) => { 3 | if (!user || !pass) throw new TypeError('user & pass are required'); 4 | 5 | return `requestAction=eel&codeType=tryLogin&login=${user}&password=${pass}`; 6 | }; 7 | 8 | module.exports = (axios, skywardURL) => (credentials) => { 9 | if (!axios || !skywardURL) throw new TypeError('axios & skywardURL are required'); 10 | 11 | return axios({ 12 | url: '../skyporthttp.w', 13 | baseURL: skywardURL, 14 | method: 'post', 15 | data: body(credentials), 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/history/scrape.js: -------------------------------------------------------------------------------- 1 | 2 | const body = ({ dwd, wfaacl, encses }) => { 3 | if (!dwd || !wfaacl || !encses) throw new TypeError('dwd, wfaacl, & encses are required'); 4 | 5 | return `dwd=${dwd}&wfaacl=${wfaacl}&encses=${encses}`; 6 | }; 7 | 8 | module.exports = (axios, skywardURL) => (auth) => { 9 | if (!axios || !skywardURL) throw new TypeError('axios & skywardURL are required'); 10 | 11 | return axios({ 12 | url: '../sfacademichistory001.w', 13 | baseURL: skywardURL, 14 | method: 'post', 15 | data: body(auth), 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/gradebook/scrape.js: -------------------------------------------------------------------------------- 1 | 2 | const body = ({ encses, sessionId }, course, bucket) => { 3 | if (!encses || !sessionId) throw new TypeError('encses & sessionId are required'); 4 | 5 | return 'action=viewGradeInfoDialog&fromHttp=yes&ishttp=true' 6 | + `&corNumId=${course}&bucket=${bucket}` 7 | + `&sessionid=${sessionId}&encses=${encses}`; 8 | }; 9 | 10 | module.exports = (axios, skywardURL) => (auth, course, bucket) => { 11 | if (!axios || !skywardURL) throw new TypeError('axios & skywardURL are required'); 12 | 13 | return axios({ 14 | url: '../httploader.p?file=sfgradebook001.w', 15 | baseURL: skywardURL, 16 | method: 'post', 17 | data: body(auth, course, bucket), 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skyward-rest", 3 | "version": "1.0.1", 4 | "description": "Unofficial Rest API for Skyward", 5 | "main": "index.js", 6 | "dependencies": { 7 | "axios": "^0.18.0", 8 | "bluebird": "^3.5.3", 9 | "cheerio": "^1.0.0-rc.2", 10 | "eslint": "^5.15.0" 11 | }, 12 | "devDependencies": { 13 | "ava": "^1.3.1", 14 | "dotenv": "^7.0.0", 15 | "eslint-config-airbnb-base": "^13.1.0", 16 | "eslint-plugin-import": "^2.16.0" 17 | }, 18 | "scripts": { 19 | "test": "ava", 20 | "watch": "npx ava --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/Kaelinator/skyward-rest.git" 25 | }, 26 | "keywords": [ 27 | "Skyward", 28 | "rest", 29 | "api", 30 | "get", 31 | "grab", 32 | "scrape", 33 | "grades" 34 | ], 35 | "author": "Kael Kirk", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/Kaelinator/skyward-rest/issues" 39 | }, 40 | "homepage": "https://github.com/Kaelinator/skyward-rest#readme" 41 | } 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const reportcard = require('./src/reportcard'); 3 | const gradebook = require('./src/gradebook'); 4 | const authenticate = require('./src/authenticate'); 5 | const history = require('./src/history'); 6 | 7 | module.exports = skywardURL => ({ 8 | scrapeReport: (user, pass) => ( 9 | authenticate(skywardURL)(user, pass) 10 | .then(auth => reportcard.fetch(skywardURL)(auth)) 11 | .then(response => ({ 12 | raw: response.data, 13 | data: reportcard.getData(response), 14 | })) 15 | ), 16 | 17 | scrapeGradebook: (user, pass, { course, bucket }) => ( 18 | authenticate(skywardURL)(user, pass) 19 | .then(auth => gradebook.fetch(skywardURL)(auth)(course, bucket)) 20 | .then(response => ({ 21 | raw: response.data, 22 | data: gradebook.getData(response), 23 | })) 24 | ), 25 | 26 | scrapeHistory: (user, pass) => ( 27 | authenticate(skywardURL)(user, pass) 28 | .then(history.fetch(skywardURL)) 29 | .then(response => ({ 30 | raw: response.data, 31 | data: history.getData(response), 32 | })) 33 | ), 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 FruitsNVeggies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # tmp data 2 | tmp 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const fs = require('fs'); 3 | const Promise = require('bluebird'); 4 | require('dotenv').config(); 5 | 6 | Promise.promisifyAll(fs); 7 | 8 | const skyward = require('./index'); 9 | 10 | const writeResults = prefix => ({ raw, data }) => Promise.all([ 11 | fs.writeFileAsync(`./tmp/test/${prefix}_raw.html`, raw), 12 | fs.writeFileAsync(`./tmp/test/${prefix}_data.json`, JSON.stringify(data, null, 2)), 13 | ]); 14 | 15 | const { SKY_USER, SKY_PASS, SKY_URL } = process.env; 16 | const myisd = skyward(SKY_URL); 17 | 18 | test.serial.skip('scrapeReport api integration', t => ( 19 | myisd.scrapeReport(SKY_USER, SKY_PASS) 20 | .then(writeResults('report')) 21 | .then(t.pass) 22 | .catch(t.fail) 23 | )); 24 | 25 | test.serial.skip('scrapeGradebook api integration', t => ( 26 | myisd.scrapeGradebook(SKY_USER, SKY_PASS, { course: 97791, bucket: 'TERM 1' }) 27 | .then(writeResults('gradebook')) 28 | .then(t.pass) 29 | .catch(t.fail) 30 | )); 31 | 32 | test.serial.skip('scrapeHistory api integration', t => ( 33 | myisd.scrapeHistory(SKY_USER, SKY_PASS) 34 | .then(writeResults('history')) 35 | .then(t.pass) 36 | .catch(t.fail) 37 | )); 38 | -------------------------------------------------------------------------------- /src/reportcard/condense.js: -------------------------------------------------------------------------------- 1 | const $ = require('cheerio'); 2 | 3 | const isClassHeader = ({ c }) => c !== undefined && c.length > 0 && c[0].cId !== undefined; 4 | 5 | const isScoreElement = ({ h }) => h !== undefined && $(h).find('a').length; 6 | 7 | const isEmpty = data => data.course !== undefined; 8 | 9 | const getData = ({ h }) => { 10 | const element = $(h).find('a')[0]; 11 | 12 | return ({ 13 | course: Number($(element).attr('data-cni')), 14 | bucket: $(element).attr('data-bkt'), 15 | score: Number($(element).text()), 16 | }); 17 | }; 18 | 19 | const merge = (parent, child) => ({ 20 | course: parent.course || child.course, 21 | scores: parent.scores.concat({ bucket: child.bucket, score: child.score }), 22 | }); 23 | 24 | module.exports = (data) => { 25 | const values = Object.entries(data); 26 | const targetPair = values.find(([key]) => /stuGradesGrid_\d{5}_\d{3}/.test(key)); 27 | 28 | if (targetPair === undefined) throw new Error('stuGradesGrid not found'); 29 | 30 | const targetData = targetPair[1]; 31 | if (targetData.tb === undefined) throw new Error('stuGradesGrid.tb not found'); 32 | 33 | const { r } = targetData.tb; 34 | if (r === undefined) return []; 35 | 36 | return targetData.tb.r 37 | .filter(isClassHeader) 38 | .map(({ c }) => c.filter(isScoreElement) 39 | .map(getData) 40 | .reduce(merge, { scores: [] })) 41 | .filter(isEmpty); 42 | }; 43 | -------------------------------------------------------------------------------- /src/authenticate/authenticate.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | 3 | const login = require('./login'); 4 | 5 | test('throws when given malformed arguments', (t) => { 6 | t.throws(() => login()(), /axios & skywardURL/, 'given no arguments'); 7 | t.throws(() => login(x => x, 'fakeUrl')({}), /user & pass/, 'given no credentials'); 8 | }); 9 | 10 | test('credentials placed correctly', (t) => { 11 | const mockCredentials = { user: 1, pass: 2 }; 12 | const mockAxios = ({ data }) => data; 13 | 14 | t.is(login(mockAxios, 'fakeUrl')(mockCredentials), 'requestAction=eel&codeType=tryLogin&login=1&password=2'); 15 | }); 16 | 17 | const decode = require('./decode'); 18 | 19 | test('decode identifies incorrect credentials & throws when given malformed auth data', (t) => { 20 | t.throws(() => decode({ data: '
  • Invalid login or password.
  • ' }), /Invalid Skyward credentials/, 'given invalid credentials'); 21 | t.throws(() => decode(), /data is required/, 'given no arguments'); 22 | t.throws(() => decode({ data: '' }), /Malformed auth data/, 'given malformed data'); 23 | }); 24 | 25 | test('decode identifies tokens', (t) => { 26 | const data = '
  • 319238^279419^23009402^27834052^58192^s219261^2^sfhome01.w^false^no ^no^no^^zdkNjlfkjbwanfcX^jDWadubjdaCOdEjY
  • '; 27 | const expected = { 28 | dwd: '319238', wfaacl: '27834052', encses: 'jDWadubjdaCOdEjY', sessionId: '279419%1523009402', 29 | }; 30 | t.deepEqual(decode({ data }), expected); 31 | }); 32 | -------------------------------------------------------------------------------- /src/reportcard/reportcard.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Promise = require('bluebird'); 3 | const fs = require('fs'); 4 | 5 | Promise.promisifyAll(fs); 6 | 7 | const scrape = require('./scrape'); 8 | 9 | test('throws when given malformed arguments', (t) => { 10 | t.throws(() => scrape()(), /axios & skywardURL/, 'given no arguments'); 11 | t.throws(() => scrape(x => x, 'fakeUrl')({}), /dwd, wfaacl, & encses/, 'given no auth data'); 12 | }); 13 | 14 | test('auth data placed correctly', (t) => { 15 | const auth = { dwd: 1, wfaacl: 2, encses: 3 }; 16 | const mockAxios = ({ data }) => data; 17 | 18 | t.is(scrape(mockAxios, 'fakeUrl')(auth), 'dwd=1&wfaacl=2&encses=3'); 19 | }); 20 | 21 | const parse = require('./parse'); 22 | const payload = require('./data/payload.data'); 23 | 24 | test('parse extracts javascript', (t) => { 25 | t.deepEqual(parse({ data: payload.slimHtml }), { x: 'marks the spot' }); 26 | t.notThrows(() => parse({ data: payload.fullHtml }), 'parse executes without throwing'); 27 | }); 28 | 29 | const condense = require('./condense'); 30 | 31 | test('condense handles malformed input', (t) => { 32 | t.throws(() => condense({}), /stuGradesGrid not found/, 'no \'stuGradesGrid\' key exists'); 33 | 34 | const noTb = { stuGradesGrid_74477_004: {} }; 35 | t.throws(() => condense(noTb), /tb not found/, 'no \'tb\' key exists'); 36 | 37 | const noR = { stuGradesGrid_74477_004: { tb: {} } }; 38 | t.deepEqual(condense(noR), [], 'no \'r\' key exists'); 39 | }); 40 | 41 | test('condense matches example data', (t) => { 42 | const payloadTest = ({ input, output }, message) => t.deepEqual(condense(input), output, message); 43 | 44 | payloadTest(payload.slimSingleCourse, 'matches with minimal single course data'); 45 | payloadTest(payload.fullSingleCourse, 'matches with full single course data'); 46 | payloadTest(payload.slimMultiCourse, 'matches with slim multi course data'); 47 | payloadTest(payload.emptyMultiCourse, 'removes null with empty multi course data'); 48 | payloadTest(payload.fullMultiCourse, 'matches with full multi course data'); 49 | }); 50 | -------------------------------------------------------------------------------- /src/history/condense.js: -------------------------------------------------------------------------------- 1 | const $ = require('cheerio'); 2 | const chunk = require('./chunk'); 3 | 4 | const parseHeader = ({ c }) => { 5 | const headerText = $(c[0].h).text(); 6 | const headerResults = /(\d+)\D+(\d+)\D+(\d+)/.exec(headerText); 7 | 8 | const begin = headerResults && headerResults[1]; 9 | const end = headerResults && headerResults[2]; 10 | const dates = { begin, end }; 11 | 12 | const grade = headerResults ? Number(headerResults[3]) : null; 13 | 14 | return { dates, grade, courses: [] }; 15 | }; 16 | 17 | const parseLits = ({ c }) => c.slice(2) 18 | .map(({ h }) => $(h).text()) 19 | .map(lit => ({ lit })); 20 | 21 | const parseCourses = ({ c }) => { 22 | const course = $(c[0].h).text(); 23 | const scores = c.slice(2) 24 | .map(({ h }) => $(h).text().trim()) 25 | .map(text => ({ grade: Number(text) || text || null })); 26 | return { course, scores }; 27 | }; 28 | 29 | const merge = (obj, row) => { 30 | if (row.courses) return row; // set base object 31 | if (row.scores) return Object.assign(obj, { courses: obj.courses.concat(row) }); // append score 32 | 33 | /* place 'lit' information into every score */ 34 | const courses = obj.courses 35 | .map(course => Object.assign(course, { 36 | scores: course.scores 37 | .map(({ grade }, i) => Object.assign({ grade }, row[i])) 38 | .filter(({ grade }) => !!grade), // get rid of null elements 39 | })); 40 | 41 | return Object.assign(obj, { courses }); 42 | }; 43 | 44 | module.exports = (data) => { 45 | const values = Object.entries(data); 46 | const targetPairs = values.filter(([key]) => /gradeGrid_\d{5}_\d{3}_\d{4}/.test(key)); 47 | 48 | // if (targetPairs.length === 0) throw new Error('gradeGrid not found'); 49 | 50 | // const targetData = targetPairs[1]; 51 | // if (targetData.tb === undefined) throw new Error('gradeGrid.tb not found'); 52 | 53 | // const { r } = targetData.tb; 54 | // if (r === undefined) return []; 55 | 56 | const isHeader = row => /(\d+)\D+(\d+)\D+(\d+)/.test($(row.c[0].h).find('div').first().text()); 57 | 58 | return targetPairs 59 | .map(pair => pair[1]) 60 | .map(({ tb: { r } }) => r) 61 | .reduce(chunk(isHeader), []) 62 | .map(([header, lits, ...courses]) => [ 63 | parseHeader(header), 64 | ...courses.map(parseCourses), 65 | parseLits(lits), 66 | ].reduce(merge)); 67 | }; 68 | -------------------------------------------------------------------------------- /src/gradebook/gradebook.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const Promise = require('bluebird'); 3 | const fs = require('fs'); 4 | 5 | Promise.promisifyAll(fs); 6 | 7 | const scrape = require('./scrape'); 8 | 9 | test('throws when given malformed arguments', (t) => { 10 | t.throws(() => scrape()(), /axios & skywardURL/, 'given no arguments'); 11 | t.throws(() => scrape(x => x, 'fakeUrl')({}), /encses & sessionId/, 'given no auth data'); 12 | }); 13 | 14 | test('auth & request data placed correctly', (t) => { 15 | const auth = { encses: 1, sessionId: 2 }; 16 | const mockAxios = ({ data }) => data; 17 | 18 | const expectedBody = 'action=viewGradeInfoDialog&fromHttp=yes&ishttp=true' 19 | + '&corNumId=98112&bucket=TERM 1&sessionid=2&encses=1'; 20 | 21 | t.is(scrape(mockAxios, 'fakeUrl')(auth, 98112, 'TERM 1'), expectedBody); 22 | }); 23 | 24 | const parse = require('./parse'); 25 | const payload = require('./data/payload.data'); 26 | 27 | const testParsePlan = t => ({ input, output }, message) => { 28 | const result = parse({ data: input }); 29 | 30 | t.deepEqual(result.course, output.course, `course value matches ${message}`); 31 | t.deepEqual(result.instructor, output.instructor, `instructor value matches ${message}`); 32 | t.deepEqual(result.lit, output.lit, `lit value matches ${message}`); 33 | t.deepEqual(result.period, output.period, `period value matches ${message}`); 34 | t.deepEqual(result.grade, output.grade, `grade value matches ${message}`); 35 | t.deepEqual(result.gradeAdjustment, output.gradeAdjustment, `gradeAdjustment value matches ${message}`); 36 | t.deepEqual(result.score, output.score, `score value matches ${message}`); 37 | t.deepEqual(result.breakdown, output.breakdown, `breakdown value matches ${message}`); 38 | t.deepEqual(result.gradebook, output.gradebook, `breakdown value matches ${message}`); 39 | }; 40 | 41 | test('parse matches example data', (t) => { 42 | const testParse = testParsePlan(t); 43 | 44 | testParse(payload.simplePR, 'with a simple Progress Report'); 45 | testParse(payload.simpleQ, 'with a simple Quarter'); 46 | testParse(payload.simpleSem, 'with a simple Semester'); 47 | 48 | testParse(payload.emptyMajorPR, 'with a Progress Report missing major grades'); 49 | testParse(payload.gradeAdjustedQ, 'with a Quarter that has grade adjustment'); 50 | testParse(payload.emptyScores, 'with a Quarter that has empty scores'); 51 | testParse(payload.decimalScores, 'with a Quarter that has decimal scores'); 52 | }); 53 | -------------------------------------------------------------------------------- /src/history/history.test.js: -------------------------------------------------------------------------------- 1 | const $ = require('cheerio'); 2 | const test = require('ava'); 3 | const scrape = require('./scrape'); 4 | 5 | test('throws when given malformed arguments', (t) => { 6 | t.throws(() => scrape()(), /axios & skywardURL/, 'given no arguments'); 7 | t.throws(() => scrape(x => x, 'fakeUrl')({}), /dwd, wfaacl, & encses are required/, 'given no credentials'); 8 | }); 9 | 10 | test('credentials placed correctly', (t) => { 11 | const auth = { dwd: 1, wfaacl: 2, encses: 3 }; 12 | const mockAxios = ({ data }) => data; 13 | 14 | t.is(scrape(mockAxios, 'fakeUrl')(auth), 'dwd=1&wfaacl=2&encses=3'); 15 | }); 16 | 17 | 18 | const parse = require('../reportcard/parse'); 19 | const payload = require('./data/payload.data'); 20 | 21 | test('reportcard parse extracts javascript', (t) => { 22 | t.deepEqual(parse({ data: payload.slimHtml }), { x: 'marks the spot' }); 23 | }); 24 | 25 | const chunk = require('./chunk'); 26 | 27 | test('chunk helper function regroups arrays', (t) => { 28 | const chunkBy2s = [[0, 1, 2, 3]].reduce(chunk(n => n % 2 === 0), []); 29 | t.deepEqual(chunkBy2s, [[0, 1], [2, 3]], 'chunks with single nested array'); 30 | 31 | const chunkBy10s = [[0, 5, 10, 15], [20, 25], [30, 35, 40, 45, 50, 55]] 32 | .reduce(chunk(n => n % 10 === 0), []); 33 | 34 | t.deepEqual(chunkBy10s, [[0, 5], [10, 15], [20, 25], [30, 35], [40, 45], [50, 55]], 'chunks with multiple nested arrays'); 35 | 36 | const chunkAdjacent = [[0, 5, 10, 20, 30, 31, 32, 35], [40, 45]] 37 | .reduce(chunk(n => n % 10 === 0), []); 38 | t.deepEqual(chunkAdjacent, [[0, 5], [10], [20], [30, 31, 32, 35], [40, 45]], 'chunks with adjacent matching values'); 39 | 40 | const isHeader = row => /(\d+)\D+(\d+)\D+(\d+)/.test($(row.c[0].h).find('div').first().text()); 41 | 42 | const { input, output } = payload.chunkConjoinedYear; 43 | t.deepEqual(input.reduce(chunk(isHeader), []), output, 'chunks by each year'); 44 | }); 45 | 46 | const condense = require('./condense'); 47 | 48 | test('reportcard condense extracts meaningful data', (t) => { 49 | const payloadTest = ({ input, output }, message) => t.deepEqual(condense(input), output, message); 50 | 51 | payloadTest(payload.slimSingle, 'matches with minimal single year, single course data'); 52 | payloadTest(payload.slimMultiCourse, 'matches with minimal single year, multi course data'); 53 | payloadTest(payload.slimMultiYear, 'matches with minimal multi year, single course data'); 54 | payloadTest(payload.fullConjoinedYear, 'matches with full multi conjoined years with multi course data'); 55 | }); 56 | -------------------------------------------------------------------------------- /src/gradebook/parse.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | 3 | const extractNumber = (regexp, text) => { 4 | const result = regexp.exec(text); 5 | const n = result && Number(result[1]); 6 | return (n === 0) ? 0 : n || null; 7 | }; 8 | 9 | const extractPoints = (pointsText) => { 10 | const earned = extractNumber(/([\d*.]+)\D+[\d*.]+/, pointsText); 11 | const total = extractNumber(/[\d*.]+\D+([\d*.]+)/, pointsText); 12 | return { earned, total }; 13 | }; 14 | 15 | const parseHeader = ($) => { 16 | const course = $('h2.gb_heading>span>a').first().text(); // e.g. 'PHYSICS 2 AP' 17 | const instructor = $('h2.gb_heading>span>a').last().text(); // e.g. 'Jay' 18 | 19 | const periodText = $('h2.gb_heading>span>span').text(); // e.g. '(Period #)' 20 | const period = extractNumber(/\(\D+(\d+)\)/, periodText); 21 | 22 | return { course, instructor, period }; 23 | }; 24 | 25 | const parseSummary = ($) => { 26 | const resultRow = $('table[id*="grid_stuTermSummaryGrid"]>tbody>tr').first(); 27 | 28 | const gradeText = resultRow.find('td').first().text(); 29 | const grade = extractNumber(/(\d+)/, gradeText); 30 | 31 | const scoreText = resultRow.find('td').last().text(); 32 | const score = extractNumber(/(\d+\.\d+)/, scoreText); 33 | 34 | const gradeAdjustmentText = $('table[id*="grid_stuTermSummaryGrid"]>tbody>tr').slice(1, 2).find('td').last() 35 | .text(); 36 | const gradeAdjustment = extractNumber(/(\d+\.\d+)/, gradeAdjustmentText); 37 | 38 | const litText = $('table[id*="grid_stuTermSummaryGrid"]>thead>tr>th').first().text(); 39 | const litResults = /(\w+)\D+\((\d{2}\/\d{2}\/\d{4})\s-\s(\d{2}\/\d{2}\/\d{4})\)/.exec(litText); 40 | const name = litResults && litResults[1]; 41 | const begin = litResults && litResults[2]; 42 | const end = litResults && litResults[3]; 43 | const lit = { name, begin, end }; 44 | 45 | return { 46 | grade, score, lit, gradeAdjustment, 47 | }; 48 | }; 49 | 50 | const parseBreakdown = ($) => { 51 | const breakdown = $('table[id*="grid_stuTermSummaryGrid"]>tbody>tr.even'); 52 | 53 | if (breakdown.first().text() === '') return null; // no header 54 | 55 | const extractData = (i, tr) => { 56 | const scoreText = $(tr).find('td').last().text(); 57 | const score = extractNumber(/(\d+\.\d+)/, scoreText); 58 | 59 | const rest = $(tr).find('td').first(); 60 | 61 | const litText = rest.find('div').first().text(); 62 | const lit = /(\w*)/.exec(litText)[1]; 63 | 64 | const gradeText = rest.find('div').slice(1, 2).text(); 65 | const grade = extractNumber(/(\d+)/, gradeText); 66 | 67 | const weightText = rest.find('div').last().text(); 68 | const weight = extractNumber(/\((\d+)%\D+\d\D+\)/, weightText); 69 | 70 | return { 71 | lit, 72 | grade, 73 | score, 74 | weight, 75 | }; 76 | }; 77 | 78 | return breakdown 79 | .filter(i => i !== 0) // skip the header 80 | .map(extractData) 81 | .toArray(); 82 | }; 83 | 84 | const parseGradebook = ($) => { 85 | const parseSemesterCategory = (parentTr) => { 86 | const category = $(parentTr).text().trim(); 87 | const breakdown = [ 88 | $(parentTr).next(), 89 | $(parentTr).next().next(), 90 | ].map((tr) => { 91 | const label = $(tr).find('td').slice(1, 2); 92 | const lit = label.find('span').first().text(); 93 | 94 | const datesText = label.find('span').first().attr('tooltip'); 95 | const datesResults = /(\d{2}\/\d{2}\/\d{4})\s-\s(\d{2}\/\d{2}\/\d{4})/.exec(datesText); 96 | const begin = datesResults ? datesResults[1] : ''; 97 | const end = datesResults ? datesResults[2] : ''; 98 | const dates = { begin, end }; 99 | 100 | const weightText = label.find('span').last().text(); 101 | const weight = extractNumber(/\(\D+(\d+\.\d+)%\)/, weightText); 102 | 103 | const gradeText = $(tr).find('td').slice(2, 3).text(); 104 | const grade = extractNumber(/(\d+)/, gradeText); 105 | 106 | const scoreText = $(tr).find('td').slice(3, 4).text(); 107 | const score = extractNumber(/(\d+.\d+)/, scoreText); 108 | 109 | const pointsText = $(tr).find('td').slice(4, 5).text(); 110 | const points = extractPoints(pointsText); 111 | 112 | return { 113 | lit, 114 | weight, 115 | dates, 116 | grade, 117 | score, 118 | points, 119 | }; 120 | }); 121 | 122 | return { 123 | category, 124 | breakdown, 125 | assignments: [], 126 | }; 127 | }; 128 | 129 | const extractData = (_, tr) => { 130 | if ($(tr).find('td').length <= 1) return null; 131 | 132 | const isCategory = $(tr).hasClass('sf_Section cat'); 133 | if (isCategory && $(tr).prev().hasClass('sf_Section cat')) return null; 134 | if (isCategory && $(tr).next().hasClass('sf_Section cat')) return parseSemesterCategory(tr); 135 | 136 | const gradeText = $(tr).find('td').slice(2, 3).text(); 137 | const grade = extractNumber(/(\d+)/, gradeText); 138 | 139 | const scoreText = $(tr).find('td').slice(3, 4).text(); 140 | const score = extractNumber(/(\d+.\d+)/, scoreText); 141 | 142 | const pointsText = $(tr).find('td').slice(4, 5).text(); 143 | const points = extractPoints(pointsText); 144 | 145 | /* if it's a category */ 146 | if (isCategory) { 147 | const label = $(tr).find('td').slice(1, 2); 148 | 149 | const category = label.clone().children().remove().end() 150 | .text() 151 | .trim(); 152 | 153 | const weightText = label.find('span').text(); 154 | const weight = extractNumber(/\D+([\d*.]+)%/, weightText); 155 | const adjustedWeight = extractNumber(/\D+[\d*.]+\D+(\d+\.\d+)%/, weightText); 156 | 157 | return { 158 | category, 159 | weight, 160 | adjustedWeight, 161 | grade, 162 | score, 163 | points, 164 | assignments: [], 165 | }; 166 | } 167 | 168 | const date = $(tr).find('td').first().text(); 169 | const title = $(tr).find('td').slice(1, 2).text(); 170 | 171 | const missingText = $(tr).find('td').slice(5, 6).text(); 172 | const noCountText = $(tr).find('td').slice(6, 7).text(); 173 | const absentText = $(tr).find('td').slice(7, 8).text(); 174 | const meta = [ 175 | { type: 'missing', note: missingText }, 176 | { type: 'noCount', note: noCountText }, 177 | { type: 'absent', note: absentText }, 178 | ].filter(({ note }) => !note.match(/^\s+$/)); 179 | 180 | return { 181 | title, 182 | grade, 183 | score, 184 | points, 185 | date, 186 | meta, 187 | }; 188 | }; 189 | 190 | const nest = (gradebook, data) => { 191 | if (data === null) return gradebook; 192 | if (data.category) return gradebook.concat(data); 193 | 194 | const previousCategory = gradebook.slice(-1)[0]; 195 | const assignments = previousCategory.assignments.concat(data); 196 | 197 | return [ 198 | ...gradebook.slice(0, -1), 199 | Object.assign(previousCategory, { assignments }), 200 | ]; 201 | }; 202 | 203 | return $('table[id*="grid_stuAssignmentSummaryGrid"]>tbody>tr') 204 | .map(extractData) 205 | .toArray() 206 | .reduce(nest, []); 207 | }; 208 | 209 | module.exports = ({ data }) => { 210 | const $ = cheerio.load(data); 211 | 212 | const { course, instructor, period } = parseHeader($); 213 | const { 214 | lit, grade, score, gradeAdjustment, 215 | } = parseSummary($); 216 | const breakdown = parseBreakdown($); 217 | const gradebook = parseGradebook($); 218 | 219 | return { 220 | course, 221 | instructor, 222 | lit, 223 | period, 224 | grade, 225 | gradeAdjustment, 226 | score, 227 | breakdown, 228 | gradebook, 229 | }; 230 | }; 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skyward REST 2 | 3 | [![Build Status](https://travis-ci.org/Kaelinator/skyward-rest.svg?branch=master)](https://travis-ci.org/Kaelinator/skyward-rest) 4 | 5 | ## Summary 6 | 7 | **Unofficial Rest API for Skyward** 8 | - Queries data for the fastest output 9 | - Breaks down and parses complex responses 10 | - Handles edge cases with ease 11 | - Built functionally 12 | 13 | ## Examples 14 | 15 | **Boilerplate** 16 | 17 | ```javascript 18 | const skyward = require('skyward-rest') 19 | 20 | const url = 'https://skyward.cooldistrict.net/...' 21 | 22 | const scraper = skyward(url) // the scraper! 23 | ``` 24 | 25 | **Scrape a user's course gradebook** 26 | 27 | ```javascript 28 | scraper.scrapeGradebook(user, pass, options) 29 | .then(console.log) // => Large Object 30 | ``` 31 | 32 | **Scrape a user's academic history** 33 | 34 | ```javascript 35 | scraper.scrapeHistory(user, pass) 36 | .then(console.log) // => Array of Sizeable Objects 37 | ``` 38 | 39 | ## API 40 | 41 | - [skyward( loginURL )](#skyward-loginurl-) 42 | - [.scrapeReport( user, pass )](#scrapereport-user-pass-) 43 | - [Report](#report) 44 | - [.scrapeGradebook( user, pass, options )](#scrapegradebook-user-pass-options-) 45 | - [Gradebook](#gradebook) 46 | - [.scrapeHistory( user, pass )](#scrapehistory-user-pass-) 47 | - [SchoolYear](#schoolyear) 48 | 49 | ### skyward( loginURL ) 50 | 51 | Function which returns an object containing the API. 52 | 53 | * **loginURL** _string_ - URL to the login page of the specific district's Skyward domain. Note that the URL should not redirect. 54 | 55 | ```javascript 56 | const skyward = require('skyward-rest') 57 | 58 | skyward('https://skyward.cooldistrict.net/scripts/wsisa.dll/WService=wsEAplus/seplog01.w') 59 | // => { usable functions } 60 | ``` 61 | 62 | ### .scrapeReport( user, pass ) 63 | 64 | Fetches and parses a student's report card, returning a promise which results in an object that's `data` property is an array of [`Report`](#report)s. Note that this differs from `.scrapeGradebook` in that individual assignments in a course are not scraped, only the bucket's score. 65 | 66 | * **user** _string_ - the username or Login ID of the student who's grades will be retrieved 67 | * **pass** _string_ - the password of the student 68 | 69 | ```javascript 70 | scraper.scrapeReport(user, pass) 71 | .then(({ data, raw }) => { 72 | console.log(data) // array of reports 73 | console.log(raw) // fetched html before parsing 74 | }) 75 | ``` 76 | 77 | #### Report 78 | 79 | An object that contains scores from a specific course over each bucket. 80 | 81 | ```javascript 82 | { 83 | course: 97776, // the five-digit course ID 84 | scores: [ 85 | { 86 | bucket: 'TERM 1', 87 | score: 100 88 | }, 89 | { 90 | bucket: 'TERM 2', 91 | score: 98 92 | }, 93 | /* etc */ 94 | ] 95 | } 96 | ``` 97 | 98 | ### .scrapeGradebook( user, pass, options ) 99 | 100 | Fetches and parses user's a gradebook, returning a promise which results in an object that's data property is a [`Gradebook`](#gradebook). 101 | 102 | * **user** _string_ - the username or Login ID of the student who's gradebook will be retrieved 103 | * **pass** _string_ - the password of the student 104 | * **options** _object_ - information identifying which gradebook to scrape 105 | * **course** _number_ - the five-digit course ID to scrape _(e.g. 97776, 97674, etc. )_ 106 | * **bucket** _string_ - the term to scrape _(e.g. 'TERM 1', 'SEM 1', etc.)_ 107 | 108 | ```javascript 109 | scraper.scrapeGradebook(user, pass, { course: 97776, bucket: 'TERM 3' }) 110 | .then(({ data, raw }) => { 111 | console.log(data) // gradebook 112 | console.log(raw) // fetched xml before parsing 113 | }) 114 | ``` 115 | 116 | #### Gradebook 117 | 118 | An object that contains information and assignments about a course at a specific bucket. 119 | 120 | ```javascript 121 | { 122 | course: 'PHYSICS 2 AP', // name of the course 123 | instructor: 'Jennifer Smith', // name of the instructor 124 | lit: { // information about the specific bucket 125 | name: 'S1', // bucket's alias 126 | begin: '08/20/2018', // bucket's begin date 127 | end: '12/20/2018' // bucket's end date 128 | }, 129 | period: 1, // course's order in the day 130 | score: 99.5, // score recieved (usually contains a decimal) 131 | grade: 100, // score after rounding (always a whole number) 132 | gradeAdjustment: 1.5, // points added to average to get score (null if no adjustment) 133 | breakdown: [ // buckets which make up this bucket's score (null if no breakdown) 134 | { 135 | lit: 'Q2', // bucket's alias 136 | score: 95.5, // score recieved 137 | grade: 96, // score after rounding 138 | weight: 50, // part that this bucket's score makes up the parent bucket's score (out of 100) 139 | }, 140 | { 141 | lit: 'Q1', 142 | grade: 100, 143 | score: 100, 144 | weight: 50, 145 | }, 146 | ], 147 | gradebook: [ // grade categories which make up this bucket's score 148 | { 149 | category: 'Major', // category title 150 | breakdown: [ // buckets which make up this category (undefined if no breakdown) 151 | { 152 | lit: 'Q2', // bucket's alias 153 | weight: 70, // part that this bucket's score makes up this category's score (out of 100) 154 | dates: { 155 | begin: '10/22/2018', // bucket's begin date 156 | end: '12/20/2018', // bucket's end date 157 | }, 158 | score: 96.5, // score recieved 159 | grade: 97, // score after rounding 160 | points: { 161 | earned: 965, // sum of all assignments' earned points 162 | total: 1000, // sum of all assignments' total points 163 | }, 164 | }, 165 | /* etc. */ 166 | ], 167 | assignments: [ // assignments which make up this category 168 | { 169 | title: 'TEST IV', 170 | score: 100, // score recieved (null if no score) 171 | grade: 100, // score after rounding (null if no grade) 172 | points: { 173 | earned: 100, // earned points (null if no earned) 174 | total: 100, // total points (null if no total) 175 | }, 176 | date: '09/07/18', // date the assignment is/was due 177 | meta: [ // assignment modifiers 178 | { 179 | type: 'absent', // modifier type (e.g. 'absent', 'noCount', or 'missing') 180 | note: 'Parent note received within 5d', // extra message 181 | } 182 | ], 183 | }, 184 | /* etc. */ 185 | ] 186 | }, 187 | /* etc. */ 188 | ] 189 | } 190 | ``` 191 | 192 | ### .scrapeHistory( user, pass ) 193 | 194 | Fetches and parses user's a academic history, returning a promise which results in an object that's data property is an array of [`SchoolYear`](#schoolyear)s. 195 | 196 | * **user** _string_ - the username or Login ID of the student who's academic history will be retrieved 197 | * **pass** _string_ - the password of the student 198 | 199 | ```javascript 200 | scraper.scrapeHistory(user, pass) 201 | .then(({ data, raw }) => { 202 | console.log(data) // array of schoolYears 203 | console.log(raw) // fetched xml before parsing 204 | }) 205 | ``` 206 | 207 | #### SchoolYear 208 | 209 | An object that contins information, courses, and scores from a completed school year 210 | 211 | ```javascript 212 | { 213 | dates: { 214 | begin: '2018', // school year begin date 215 | end: '2019', // school year end date 216 | }, 217 | grade: 12, // grade of student during the school year 218 | courses: [ // courses taken during the year 219 | { 220 | course: 'PHYSICS 2 AP', // course name 221 | scores: [ 222 | { 223 | grade: 100, // grade recieved 224 | lit: 'S1', // bucket alias 225 | }, 226 | /* etc. */ 227 | ] 228 | }, 229 | /* etc. */ 230 | ] 231 | } 232 | ``` 233 | -------------------------------------------------------------------------------- /src/gradebook/data/payload.data.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | simplePR: { 3 | output: { 4 | course: 'PHYSICS 2 AP', 5 | instructor: 'Juergen Smith', 6 | lit: { name: 'PR1', begin: '08/20/2018', end: '09/07/2018' }, 7 | period: 2, 8 | grade: 48, 9 | gradeAdjustment: null, 10 | score: 98.00, 11 | breakdown: null, 12 | gradebook: [ 13 | { 14 | category: 'Major', 15 | weight: 70.00, 16 | adjustedWeight: null, 17 | grade: 100, 18 | score: 100.00, 19 | points: { earned: 100, total: 100 }, 20 | assignments: [ 21 | { 22 | title: 'TEST I Dimensional Analysis', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '09/06/18', meta: [], 23 | }, 24 | ], 25 | }, 26 | { 27 | category: 'Minor', 28 | weight: 30.00, 29 | adjustedWeight: null, 30 | grade: 92, 31 | score: 92.00, 32 | points: { earned: 184, total: 200 }, 33 | assignments: [ 34 | { 35 | title: 'Dimensional Analysis 2.1 RED', grade: 89, score: 89.00, points: { earned: 89, total: 100 }, date: '08/31/18', meta: [{ type: 'absent', note: 'Parent Note received within 5d' }], 36 | }, 37 | { 38 | title: 'Dimensional Analysis 2', grade: 95, score: 95.00, points: { earned: 95, total: 100 }, date: '08/28/18', meta: [], 39 | }, 40 | ], 41 | }, 42 | ], 43 | }, 44 | input: '\n\n\n
    JudyJudy R. Hopps (COOL HIGH SCHOOL)
    \n\n\n

    Summary

    \n
    \n\n\n\n\n
    PR1 Grade
    (08/20/2018 - 09/07/2018)
    Score (%)
     
    4898.00
    \n
    \n
    \n
    \n\n\n
    \n
    \n\n\n\n\n\n\n\n\n
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Major
    weighted at 70.00%
    100100.00100 out of 100 
    09/06/18TEST I Dimensional Analysis100  100.00100 out of 100   
     Minor
    weighted at 30.00%
    9292.00184 out of 200 
    08/31/18Dimensional Analysis 2.1 RED89  89.0089 out of 100  Parent Note received within 5d
    08/28/18Dimensional Analysis 295  95.0095 out of 100   
    \n
    \n
    \n
    \n]]>
    \n\n$(document).ready(function(){\nsff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "teacher", "id": "3563", "entity": "004","showEmail": "no"},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"showTitle": false, "html": pResponse.output, type: "default", "autoHide": true, "pointAt": pointAt}); } });},"#VTjaaxhRljqiFJkt");\nsff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "class", "student": "74872", "cornumid": "97678", "track": "0", "section": sff.revertCharReplaceForId("01"), "dialogOptions": ""},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"id": "sf_classinfo", "showTitle": true, "html": pResponse.output, type: "default", "autoHide": false, "title": "Class Info", "pointAt": pointAt,"direction": "right"}); } });},"#JcdMQckkdlJiciFd");\n});\n\n]]>\nsff.sv("filesAdded", "fusion.js");\nsff.sv("gridCount", "2");\n\n]]>\n
    \n', 45 | }, 46 | simpleQ: { 47 | output: { 48 | course: 'U.S. GOVT AP', 49 | instructor: 'Joel Smith', 50 | lit: { name: 'Q1', begin: '08/20/2018', end: '10/19/2018' }, 51 | period: 3, 52 | grade: 90, 53 | gradeAdjustment: null, 54 | score: 90.00, 55 | breakdown: null, 56 | gradebook: [ 57 | { 58 | category: 'Major', 59 | weight: 70.00, 60 | adjustedWeight: null, 61 | grade: 87, 62 | score: 87.33, 63 | points: { earned: 262, total: 300 }, 64 | assignments: [ 65 | { 66 | title: 'Opinion Poll', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '10/12/18', meta: [], 67 | }, 68 | { 69 | title: 'Federalism TEST', grade: 84, score: 84.00, points: { earned: 84, total: 100 }, date: '09/18/18', meta: [], 70 | }, 71 | { 72 | title: 'Unit 1 Test', grade: 78, score: 78.00, points: { earned: 78, total: 100 }, date: '09/06/18', meta: [{ type: 'absent', note: 'Teacher-recorded tardy ' }], 73 | }, 74 | ], 75 | }, 76 | { 77 | category: 'Minor', 78 | weight: 30.00, 79 | adjustedWeight: null, 80 | grade: 96, 81 | score: 95.67, 82 | points: { earned: 287, total: 300 }, 83 | assignments: [ 84 | { 85 | title: 'Political Ideology online test', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '10/10/18', meta: [], 86 | }, 87 | { 88 | title: 'B of R Quiz', grade: 87, score: 87.00, points: { earned: 87, total: 100 }, date: '08/31/18', meta: [{ type: 'absent', note: 'Parent Note received within 5d' }], 89 | }, 90 | { 91 | title: 'Bill of Rights Sign Language', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '08/20/18', meta: [], 92 | }, 93 | ], 94 | }, 95 | ], 96 | }, 97 | input: '
    JudyJudy R. Hopps (KLEIN COLLINS HIGH SCHOOL)

    U.S. GOVT AP (Period 3) Joel Smith

    Summary

    Q1 Grade
    (08/20/2018 - 10/19/2018)
    Score (%)
     
    9090.00
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Major
    weighted at 70.00%
    8787.33262 out of 300 
    10/12/18Opinion Poll100  100.00100 out of 100   
    09/18/18Federalism TEST84  84.0084 out of 100   
    09/06/18Unit 1 Test78  78.0078 out of 100  Teacher-recorded tardy
     Minor
    weighted at 30.00%
    9695.67287 out of 300 
    10/10/18Political Ideology online test100  100.00100 out of 100   
    08/31/18B of R Quiz87  87.0087 out of 100  Parent Note received within 5d
    08/20/18Bill of Rights Sign Language100  100.00100 out of 100   
    ]]>
    $(document).ready(function(){sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "class", "student": "74872", "cornumid": "97524", "track": "0", "section": sff.revertCharReplaceForId("02"), "dialogOptions": ""},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"id": "sf_classinfo", "showTitle": true, "html": pResponse.output, type: "default", "autoHide": false, "title": "Class Info", "pointAt": pointAt,"direction": "right"}); } });},"#kdaNbaixmdbckdep");sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "teacher", "id": "1835", "entity": "004","showEmail": "no"},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"showTitle": false, "html": pResponse.output, type: "default", "autoHide": true, "pointAt": pointAt}); } });},"#iBUonbeajdVQjkUV");});]]>sff.sv("filesAdded", "jquery.1.8.2.js,qsfmain001.css,sfgradebook.css,qsfmain001.min.js,sfgradebook.js,sfprint001.js,fusion.js");sff.sv("gridCount", "13");]]>
    ', 98 | }, 99 | simpleSem: { 100 | output: { 101 | course: 'U.S. GOVT AP', 102 | instructor: 'Joel Smith', 103 | lit: { name: 'S1', begin: '08/20/2018', end: '12/20/2018' }, 104 | period: 3, 105 | grade: 85, 106 | gradeAdjustment: null, 107 | score: 85.1, 108 | breakdown: [ 109 | { 110 | lit: 'SE1', grade: 86, score: 86.00, weight: 10, 111 | }, 112 | { 113 | lit: 'Q2', grade: 80, score: 80.00, weight: 45, 114 | }, 115 | { 116 | lit: 'Q1', grade: 90, score: 90.00, weight: 45, 117 | }, 118 | ], 119 | gradebook: [ 120 | { 121 | category: 'Major', 122 | breakdown: [ 123 | { 124 | lit: 'Q2', weight: 70.00, grade: 75, score: 74.67, points: { earned: 224, total: 300 }, dates: { begin: '10/22/2018', end: '12/20/2018' }, 125 | }, 126 | { 127 | lit: 'Q1', weight: 70.00, grade: 87, score: 87.33, points: { earned: 262, total: 300 }, dates: { begin: '08/20/2018', end: '10/19/2018' }, 128 | }, 129 | ], 130 | assignments: [ 131 | { 132 | title: 'Judiciary TEST', grade: 91, score: 91.00, points: { earned: 91, total: 100 }, date: '12/12/18', meta: [], 133 | }, 134 | { 135 | title: 'PREZ TEST', grade: 71, score: 71.00, points: { earned: 71, total: 100 }, date: '11/27/18', meta: [], 136 | }, 137 | { 138 | title: 'CONGRESS TEST', grade: 62, score: 62.00, points: { earned: 62, total: 100 }, date: '11/14/18', meta: [], 139 | }, 140 | { 141 | title: 'Opinion Poll', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '10/12/18', meta: [], 142 | }, 143 | { 144 | title: 'Federalism TEST', grade: 84, score: 84.00, points: { earned: 84, total: 100 }, date: '09/18/18', meta: [], 145 | }, 146 | { 147 | title: 'Unit 1 Test', grade: 78, score: 78.00, points: { earned: 78, total: 100 }, date: '09/06/18', meta: [{ type: 'absent', note: 'Teacher-recorded tardy ' }], 148 | }, 149 | ], 150 | }, 151 | { 152 | category: 'Minor', 153 | breakdown: [ 154 | { 155 | lit: 'Q2', weight: 30.00, grade: 92, score: 92.00, points: { earned: 1012, total: 1100 }, dates: { begin: '10/22/2018', end: '12/20/2018' }, 156 | }, 157 | { 158 | lit: 'Q1', weight: 30.00, grade: 96, score: 95.67, points: { earned: 287, total: 300 }, dates: { begin: '08/20/2018', end: '10/19/2018' }, 159 | }, 160 | ], 161 | assignments: [ 162 | { 163 | title: 'Case Notecards', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '12/04/18', meta: [], 164 | }, 165 | { 166 | title: 'Standard Deviants Judicial Qui', grade: 87, score: 87.00, points: { earned: 87, total: 100 }, date: '11/30/18', meta: [], 167 | }, 168 | { 169 | title: 'Letter to your Congressman', grade: 100, score: 100.00, points: { earned: 200, total: 200 }, date: '11/13/18', meta: [], 170 | }, 171 | { 172 | title: 'Congress FRQ', grade: 75, score: 75.00, points: { earned: 225, total: 300 }, date: '11/07/18', meta: [{ type: 'absent', note: 'Parent Note received within 5d' }], 173 | }, 174 | { 175 | title: 'Write a Bill', grade: 100, score: 100.00, points: { earned: 200, total: 200 }, date: '11/02/18', meta: [], 176 | }, 177 | { 178 | title: 'Hamilton Group', grade: 100, score: 100.00, points: { earned: 200, total: 200 }, date: '11/01/18', meta: [], 179 | }, 180 | { 181 | title: 'Political Ideology online test', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '10/10/18', meta: [], 182 | }, 183 | { 184 | title: 'B of R Quiz', grade: 87, score: 87.00, points: { earned: 87, total: 100 }, date: '08/31/18', meta: [{ type: 'absent', note: 'Parent Note received within 5d' }], 185 | }, 186 | { 187 | title: 'Bill of Rights Sign Language', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '08/20/18', meta: [], 188 | }, 189 | ], 190 | }, 191 | ], 192 | }, 193 | input: '
    JudyJudy R. Hopps (COOL HIGH SCHOOL)

    U.S. GOVT AP (Period 3) Joel Smith

    Summary

    S1 Grade
    (08/20/2018 - 12/20/2018)
    Score (%)
     
    8585.10
    Grade Breakdown: 
    86
    (10% of Sem 1 grade)
    86.00
    80
    (45% of Sem 1 grade)
    80.00
    90
    (45% of Sem 1 grade)
    90.00
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Major    
     Q2 (weighted at 70.00%)7574.67224 out of 300 
     Q1 (weighted at 70.00%)8787.33262 out of 300 
    12/12/18Judiciary TEST91  91.0091 out of 100   
    11/27/18PREZ TEST71  71.0071 out of 100   
    11/14/18CONGRESS TEST62  62.0062 out of 100   
    10/12/18Opinion Poll100  100.00100 out of 100   
    09/18/18Federalism TEST84  84.0084 out of 100   
    09/06/18Unit 1 Test78  78.0078 out of 100  Teacher-recorded tardy
     Minor     
     Q2 (weighted at 30.00%)9292.001012 out of 1100 
     Q1 (weighted at 30.00%)9695.67287 out of 300 
    12/04/18Case Notecards100  100.00100 out of 100   
    11/30/18Standard Deviants Judicial Qui87  87.0087 out of 100   
    11/13/18Letter to your Congressman100  100.00200 out of 200   
    11/07/18Congress FRQ75  75.00225 out of 300  Parent Note received within 5d
    11/02/18Write a Bill100  100.00200 out of 200   
    11/01/18Hamilton Group100  100.00200 out of 200   
    10/10/18Political Ideology online test100  100.00100 out of 100   
    08/31/18B of R Quiz87  87.0087 out of 100  Parent Note received within 5d
    08/20/18Bill of Rights Sign Language100  100.00100 out of 100   
    ]]>
    $(document).ready(function(){sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "class", "student": "74872", "cornumid": "97524", "track": "0", "section": sff.revertCharReplaceForId("02"), "dialogOptions": ""},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"id": "sf_classinfo", "showTitle": true, "html": pResponse.output, type: "default", "autoHide": false, "title": "Class Info", "pointAt": pointAt,"direction": "right"}); } });},"#hjKAKiDcKallijja");sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "teacher", "id": "1835", "entity": "004","showEmail": "no"},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"showTitle": false, "html": pResponse.output, type: "default", "autoHide": true, "pointAt": pointAt}); } });},"#ccJktyjidiRPiKyO");});]]>sff.sv("filesAdded", "jquery.1.8.2.js,qsfmain001.css,sfgradebook.css,qsfmain001.min.js,sfgradebook.js,sfprint001.js");sff.sv("gridCount", "2");]]>
    ', 194 | }, 195 | emptyMajorPR: { 196 | output: { 197 | course: 'STATISTICS AP', 198 | instructor: 'Johnny Smith', 199 | lit: { name: 'PR1', begin: '08/20/2018', end: '09/07/2018' }, 200 | period: 4, 201 | grade: 94, 202 | gradeAdjustment: null, 203 | score: 97.00, 204 | breakdown: null, 205 | gradebook: [ 206 | { 207 | category: 'Homework', 208 | weight: 10.00, 209 | adjustedWeight: 33.33, 210 | grade: 100, 211 | score: 100.00, 212 | points: { earned: 400, total: 400 }, 213 | assignments: [ 214 | { 215 | title: 'Sec. 1.3 HW', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '09/06/18', meta: [], 216 | }, 217 | { 218 | title: 'Sec. 1.2 HW', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '08/31/18', meta: [{ type: 'absent', note: 'Parent Note received within 5d' }], 219 | }, 220 | { 221 | title: 'Sec. 1.1 HW', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '08/29/18', meta: [], 222 | }, 223 | { 224 | title: 'Sec. 1.Intro HW', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '08/23/18', meta: [], 225 | }, 226 | ], 227 | }, 228 | { 229 | category: 'Major', 230 | weight: 70.00, 231 | adjustedWeight: 0.00, 232 | grade: null, 233 | score: null, 234 | points: { earned: null, total: null }, 235 | assignments: [], 236 | }, 237 | { 238 | category: 'Minor', 239 | weight: 20.00, 240 | adjustedWeight: 66.67, 241 | grade: 96, 242 | score: 95.67, 243 | points: { earned: 287, total: 300 }, 244 | assignments: [ 245 | { 246 | title: 'Ch. 1 Quiz Sec. 1.1 to 1.3', grade: 92, score: 92.00, points: { earned: 92, total: 100 }, date: '09/06/18', meta: [], 247 | }, 248 | { 249 | title: '1.2 CYU', grade: 90, score: 90.00, points: { earned: 90, total: 100 }, date: '08/31/18', meta: [{ type: 'noCount', note: '' }, { type: 'absent', note: 'Parent Note received within 5d' }], 250 | }, 251 | { 252 | title: '1.1 CYU', grade: 95, score: 95.00, points: { earned: 95, total: 100 }, date: '08/29/18', meta: [], 253 | }, 254 | { 255 | title: 'Smelling Parkinson Act.', grade: 100, score: 100.00, points: { earned: 100, total: 100 }, date: '08/21/18', meta: [], 256 | }, 257 | ], 258 | }, 259 | ], 260 | }, 261 | input: '
    JudyJudy R. Hopps (KLEIN COLLINS HIGH SCHOOL)

    Summary

    PR1 Grade
    (08/20/2018 - 09/07/2018)
    Score (%)
     
    9497.00
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Homework
    weighted at 10.00%, adjusted to 33.33%
    100100.00400 out of 400 
    09/06/18Sec. 1.3 HW100  100.00100 out of 100   
    08/31/18Sec. 1.2 HW100  100.00100 out of 100  Parent Note received within 5d
    08/29/18Sec. 1.1 HW100  100.00100 out of 100   
    08/23/18Sec. 1.Intro HW100  100.00100 out of 100   
     Major
    weighted at 70.00%, adjusted to 0.00%
        
    There are no Major assignments
     Minor
    weighted at 20.00%, adjusted to 66.67%
    9695.67287 out of 300 
    09/06/18Ch. 1 Quiz Sec. 1.1 to 1.392  92.0092 out of 100   
    08/31/181.2 CYU90special code 90.0090 out of 100 No countParent Note received within 5d
    08/29/181.1 CYU95  95.0095 out of 100   
    08/21/18Smelling Parkinson Act.100  100.00100 out of 100   
    ]]>
    $(document).ready(function(){sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "teacher", "id": "2411", "entity": "004","showEmail": "no"},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"showTitle": false, "html": pResponse.output, type: "default", "autoHide": true, "pointAt": pointAt}); } });},"#affjdmaOlIybUnap");sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "class", "student": "74872", "cornumid": "97623", "track": "0", "section": sff.revertCharReplaceForId("03"), "dialogOptions": ""},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"id": "sf_classinfo", "showTitle": true, "html": pResponse.output, type: "default", "autoHide": false, "title": "Class Info", "pointAt": pointAt,"direction": "right"}); } });},"#lThIklachfflQkli");});]]>sff.sv("filesAdded", "jquery.1.8.2.js,qsfmain001.css,sfgradebook.css,qsfmain001.min.js,sfgradebook.js,sfprint001.js,fusion.js");sff.sv("gridCount", "7");]]>
    ', 262 | }, 263 | gradeAdjustedQ: { 264 | output: { 265 | course: 'PHYSICS 2 AP', 266 | instructor: 'Juergen Smith', 267 | lit: { name: 'Q3', begin: '01/08/2019', end: '03/22/2019' }, 268 | period: 2, 269 | grade: 93, 270 | gradeAdjustment: 1.00, 271 | score: 93.00, 272 | breakdown: null, 273 | gradebook: [ 274 | { 275 | category: 'Major', 276 | weight: 70.00, 277 | adjustedWeight: null, 278 | grade: 93, 279 | score: 93.00, 280 | points: { earned: 372, total: 400 }, 281 | assignments: [/* too many minors, but still not including majors */], 282 | }, 283 | { 284 | category: 'Minor', 285 | weight: 30.00, 286 | adjustedWeight: null, 287 | grade: 89, 288 | score: 88.63, 289 | points: { earned: 1418, total: 1600 }, 290 | assignments: [/* too many assignments to be included; Tough Quarter */], 291 | }, 292 | ], 293 | }, 294 | input: '
    JudyJudy R. Hopps (KLEIN COLLINS HIGH SCHOOL)

    Summary

    Q3 Grade
    (01/08/2019 - 03/22/2019)
    Score (%)
     
    9393.00
    1.00 
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Major
    weighted at 70.00%
    9393.00372 out of 400 
    Shhh... I am empty
     Minor
    weighted at 30.00%
    8988.631418 out of 1600 
    Me too
    ]]>
    $(document).ready(function(){sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "class", "student": "74872", "cornumid": "97678", "track": "0", "section": sff.revertCharReplaceForId("01"), "dialogOptions": ""},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"id": "sf_classinfo", "showTitle": true, "html": pResponse.output, type: "default", "autoHide": false, "title": "Class Info", "pointAt": pointAt,"direction": "right"}); } });},"#acopbrndjlfcbnMh");sff.aee("b","click",function(e){var pointAt = this; sff.request("sfdialogs.w",{"action": "dialog", "type": "teacher", "id": "3563", "entity": "004","showEmail": "no"},function(pResponse) {if (pResponse.status === "success") { sff.dialog.show({"showTitle": false, "html": pResponse.output, type: "default", "autoHide": true, "pointAt": pointAt}); } });},"#ndqckZjckOcYpFii");});]]>sff.sv("filesAdded", "jquery.1.8.2.js,qsfmain001.css,sfgradebook.css,qsfmain001.min.js,sfgradebook.js,sfprint001.js,fusion.js");sff.sv("gridCount", "2");]]>
    ', 295 | }, 296 | emptyScores: { 297 | output: { /* data is simplified to isolate variables */ 298 | course: 'ENGLISH 4 AP', 299 | instructor: 'Laurie Smith', 300 | lit: { name: 'Q3', begin: '01/08/2019', end: '03/22/2019' }, 301 | period: 4, 302 | grade: 89, 303 | gradeAdjustment: null, 304 | score: 89.00, 305 | breakdown: null, 306 | gradebook: [ 307 | { 308 | category: 'Major', 309 | weight: 70.00, 310 | adjustedWeight: null, 311 | grade: 87, 312 | score: 87.00, 313 | points: { earned: 261, total: 300 }, 314 | assignments: [ 315 | { 316 | title: 'Extra Credit play or poem', grade: null, score: null, points: { earned: null, total: 0 }, date: '02/13/19', meta: [{ type: 'absent', note: 'Assembly, Office, pep rally' }], 317 | }, 318 | { 319 | title: 'Blog Extra Credit', grade: null, score: null, points: { earned: null, total: 0 }, date: '01/14/19', meta: [], 320 | }, 321 | ], 322 | }, 323 | { 324 | category: 'Minor', 325 | weight: 30.00, 326 | adjustedWeight: null, 327 | grade: 94, 328 | score: 94.22, 329 | points: { earned: 832, total: 883 }, 330 | assignments: [ 331 | { 332 | title: 'Realistic and Non Realistic', grade: null, score: null, points: { earned: null, total: 50 }, date: '01/15/19', meta: [{ type: 'noCount', note: '' }, { type: 'absent', note: 'Unknown abs. marked by teacher' }], 333 | }, 334 | ], 335 | }, 336 | ], 337 | }, 338 | input: '
    JudyJudy R. Hopps (COOL HIGH SCHOOL)

    Summary

    Q3 Grade
    (01/08/2019 - 03/22/2019)
    Score (%)
     
    8989.00
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Major
    weighted at 70.00%
    8787.00261 out of 300 
    02/13/19Extra Credit play or poem   * out of 0  Assembly, Office, pep rally
    01/14/19Blog Extra Credit   * out of 0   
     Minor
    weighted at 30.00%
    9494.22832 out of 883 
    01/15/19Realistic and Non Realistic\'special  * out of 50 \'NoUnknown abs. marked by teacher
    ]]>
    $(document).ready(function(){sff.aee(\'b\',\'click\',function(e){var pointAt = this; sff.request(\'sfdialogs.w\',{\'action\': \'dialog\', \'type\': \'teacher\', \'id\': \'2423\', \'entity\': \'004\',"showEmail": "no"},function(pResponse) {if (pResponse.status === \'success\') { sff.dialog.show({\'showTitle\': false, \'html\': pResponse.output, type: \'default\', \'autoHide\': true, \'pointAt\': pointAt}); } });},\'#kydnPWbVzlikWkbb\');sff.aee(\'b\',\'click\',function(e){var pointAt = this; sff.request(\'sfdialogs.w\',{\'action\': \'dialog\', \'type\': \'class\', \'student\': \'18781\', \'cornumid\': \'97423\', \'track\': \'0\', \'section\': sff.revertCharReplaceForId(\'03\'), \'dialogOptions\': \'\'},function(pResponse) {if (pResponse.status === \'success\') { sff.dialog.show({\'id\': \'sf_classinfo\', \'showTitle\': true, \'html\': pResponse.output, type: \'default\', \'autoHide\': false, \'title\': \'Class Info\', \'pointAt\': pointAt,"direction": "right"}); } });},\'#bmkWncakLEblSljB\');});]]>sff.sv(\'filesAdded\', \'jquery.1.8.2.js,qsfmain001.css,sfgradebook.css,qsfmain001.min.js,sfgradebook.js,sfprint001.js,fusion.js\');sff.sv(\'gridCount\', \'2\');]]>
    ', 339 | }, 340 | decimalScores: { 341 | output: { /* data is simplified to isolate variables */ 342 | course: 'ENGLISH 4 AP', 343 | instructor: 'Laurie Smith', 344 | lit: { name: 'Q3', begin: '01/08/2019', end: '03/22/2019' }, 345 | period: 4, 346 | grade: 89, 347 | gradeAdjustment: null, 348 | score: 89.00, 349 | breakdown: null, 350 | gradebook: [ 351 | { 352 | category: 'Major', 353 | weight: 70.00, 354 | adjustedWeight: null, 355 | grade: 95, 356 | score: 94.53, 357 | points: { earned: 95, total: 100.5 }, 358 | assignments: [ 359 | { 360 | title: 'Poetry Blog', grade: 95, score: 95.00, points: { earned: 95, total: 100.5 }, date: '01/10/19', meta: [], 361 | }, 362 | ], 363 | }, 364 | { 365 | category: 'Minor', 366 | weight: 30.00, 367 | adjustedWeight: null, 368 | grade: 78, 369 | score: 78.46, 370 | points: { earned: 104.75, total: 133.5 }, 371 | assignments: [ 372 | { 373 | title: 'PQ #16', grade: 100, score: 100.00, points: { earned: 11, total: 11 }, date: '03/01/19', meta: [], 374 | }, 375 | { 376 | title: 'PQ 10 Drama', grade: 89, score: 88.89, points: { earned: 8, total: 9 }, date: '02/12/19', meta: [], 377 | }, 378 | { 379 | title: 'AP Pilot PQ #9', grade: 62, score: 61.54, points: { earned: 8, total: 13 }, date: '01/28/19', meta: [], 380 | }, 381 | { 382 | title: 'Drama Terms', grade: 79, score: 79.00, points: { earned: 39.5, total: 50 }, date: '01/10/19', meta: [], 383 | }, 384 | { 385 | title: 'Nature of Drama', grade: 77, score: 77.00, points: { earned: 38.25, total: 50.5 }, date: '01/10/19', meta: [], 386 | }, 387 | ], 388 | }, 389 | ], 390 | }, 391 | input: '
    JudyJudy R. Hopps (COOL HIGH SCHOOL)

    Summary

    Q3 Grade
    (01/08/2019 - 03/22/2019)
    Score (%)
     
    8989.00
    DueAssignmentGradeScore(%)Points EarnedMissingNo CountAbsent
     Major
    weighted at 70.00%
    9594.5395 out of 100.5 
    01/10/19Poetry Blog95  95.0095 out of 100.5   
     Minor
    weighted at 30.00%
    7878.46104.75 out of 133.5 
    03/01/19PQ #16100  100.0011 out of 11   
    02/12/19PQ 10 Drama89  88.898 out of 9   
    01/28/19AP Pilot PQ #962  61.548 out of 13   
    01/10/19Drama Terms79  79.0039.5 out of 50   
    01/10/19Nature of Drama77  77.0038.25 out of 50.5   
    ]]>
    $(document).ready(function(){sff.aee(\'b\',\'click\',function(e){var pointAt = this; sff.request(\'sfdialogs.w\',{\'action\': \'dialog\', \'type\': \'teacher\', \'id\': \'2423\', \'entity\': \'004\',"showEmail": "no"},function(pResponse) {if (pResponse.status === \'success\') { sff.dialog.show({\'showTitle\': false, \'html\': pResponse.output, type: \'default\', \'autoHide\': true, \'pointAt\': pointAt}); } });},\'#kydnPWbVzlikWkbb\');sff.aee(\'b\',\'click\',function(e){var pointAt = this; sff.request(\'sfdialogs.w\',{\'action\': \'dialog\', \'type\': \'class\', \'student\': \'18781\', \'cornumid\': \'97423\', \'track\': \'0\', \'section\': sff.revertCharReplaceForId(\'03\'), \'dialogOptions\': \'\'},function(pResponse) {if (pResponse.status === \'success\') { sff.dialog.show({\'id\': \'sf_classinfo\', \'showTitle\': true, \'html\': pResponse.output, type: \'default\', \'autoHide\': false, \'title\': \'Class Info\', \'pointAt\': pointAt,"direction": "right"}); } });},\'#bmkWncakLEblSljB\');});]]>sff.sv(\'filesAdded\', \'jquery.1.8.2.js,qsfmain001.css,sfgradebook.css,qsfmain001.min.js,sfgradebook.js,sfprint001.js,fusion.js\');sff.sv(\'gridCount\', \'2\');]]>
    ', 392 | }, 393 | }; 394 | --------------------------------------------------------------------------------