Skip to content

Commit

Permalink
Merge pull request #730 from frog-pond/drew/mealtime-data-bonapp
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkrives authored May 20, 2024
2 parents fc388dc + 7774c33 commit 49832b9
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 0 deletions.
4 changes: 4 additions & 0 deletions source/ccci-stolaf-college/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as menus from './menu.js'
import * as news from './news.js'
import * as orgs from './orgs.js'
import * as printing from './printing.js'
import * as reports from './reports.js'
import * as streams from './streams.js'
import * as transit from './transit.js'
import * as util from './util.js'
Expand Down Expand Up @@ -115,6 +116,9 @@ api.get('/streams/upcoming', streams.upcoming)
// stoprint
api.get('/printing/color-printers', printing.colorPrinters)

// reports
api.get('/reports/stav', reports.stavMealtimeReport)

// utilities
api.get('/util/html-to-md', util.htmlToMarkdown)

Expand Down
15 changes: 15 additions & 0 deletions source/ccci-stolaf-college/v1/reports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {ONE_HOUR} from '../../ccc-lib/constants.js'
import * as report from '../../reports-bonapp/index.js'
import mem from 'memoize'

const getReport = mem(report.report, {maxAge: ONE_HOUR})

export const REPORT_URLS = {
stav: 'https://www.stolaf.edu/apps/mealtimes/',
}

export async function stavMealtimeReport(ctx) {
ctx.cacheControl(ONE_HOUR)

ctx.body = await getReport(REPORT_URLS.stav)
}
31 changes: 31 additions & 0 deletions source/ccci-stolaf-college/v1/reports.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {suite, test} from 'node:test'
import assert from 'node:assert/strict'
import lodash from 'lodash'

import * as reports from './reports.js'
import {StavReportType} from '../../reports-bonapp/types.js'

const {noop} = lodash

const reportFunctions = {
stav: reports.stavMealtimeReport,
}

suite('sanity checks', () => {
test('all report info endpoints are represented in this test file', () => {
assert.deepStrictEqual(
Object.keys(reportFunctions),
Object.keys(reports.REPORT_URLS),
)
})
})

suite('endpoints should not throw', {concurrency: 4}, () => {
for (const report of Object.keys(reports.REPORT_URLS)) {
test(`${report} report endpoint should return a StavReportType struct`, async () => {
let ctx = {cacheControl: noop, body: null}
await assert.doesNotReject(() => reportFunctions[report](ctx))
assert.doesNotThrow(() => StavReportType.parse(ctx.body))
})
}
})
12 changes: 12 additions & 0 deletions source/reports-bonapp/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {StavReportType} from './types.js'

/** @returns {StavReportType} */
export function CustomReportType({message}) {
return StavReportType.parse([
{
title: message,
times: [],
data: [],
},
])
}
117 changes: 117 additions & 0 deletions source/reports-bonapp/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {get} from '../ccc-lib/http.js'
import {ONE_MINUTE} from '../ccc-lib/constants.js'

import {JSDOM, VirtualConsole} from 'jsdom'
import mem from 'memoize'
import * as Sentry from '@sentry/node'

import {CustomReportType} from './helpers.js'
import {StavReportType} from './types.js'

const getBonAppPage = mem(get, {maxAge: ONE_MINUTE})

const NUMBER_OF_CHARTS_TO_PARSE = 7

/**
* @param {string|URL} url
* @return {Promise<JSDOM>}
*/
async function getBonAppReportWebpage(url) {
const virtualConsole = new VirtualConsole()
virtualConsole.sendTo(console, {omitJSDOMErrors: true})
virtualConsole.on('jsdomError', (err) => {
let jsdomErrorMessagesToSkip = [
'Uncaught [ReferenceError: wp is not defined]',
'Uncaught [ReferenceError: jQuery is not defined]',
'Uncaught [Error: Create skia surface failed]',
"Uncaught [TypeError: Cannot read properties of undefined (reading 'slice')]",
'Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)',
]
if (jsdomErrorMessagesToSkip.includes(err.message)) {
return
}
console.error(err)
})

const body = await getBonAppPage(url).text()
return new JSDOM(body, {
runScripts: 'dangerously',
resources: 'usable',
virtualConsole,
beforeParse(window) {
window.fetch = global.fetch
},
})
}

/**
* @param {string|URL} reportUrl
* @returns {Promise<StavReportType>}
*/
async function _report(reportUrl) {
let dom = await getBonAppReportWebpage(reportUrl)

dom.window.console.error = (error) => {
let errorMessagesToSkip = [
"Failed to create chart: can't acquire context from the given item",
]
if (errorMessagesToSkip.includes(error)) {
return
}
console.error(error)
}

dom.window.console.info = (info) => {
let infoMessagesToSkip = [
'Initialized global navigation scripts',
'Initialized mobile menu script scripts',
'Initialized tools navigation scripts',
'Initialized all javascript that targeted document ready.',
]
if (infoMessagesToSkip.includes(info)) {
return
}
console.info(info)
}

return new Promise((resolve, _reject) => {
dom.window.onload = () => {
const charts = dom.window.Chart.instances

const parse = (chart) => {
const {labels, datasets} = chart.data
return {
title: datasets[0].label,
times: labels,
data: datasets[0].data,
}
}

const payload = []

for (let i = 0; i < NUMBER_OF_CHARTS_TO_PARSE; ++i) {
try {
payload.push(parse(charts[i]))
} catch (err) {
console.warn({error: err.message})
}
}

resolve(StavReportType.parse(payload))
}
})
}

/**
* @param {string|URL} reportUrl
* @returns {Promise<StavReportType>}
*/
export function report(reportUrl) {
try {
return _report(reportUrl)
} catch (err) {
console.error(err)
Sentry.isInitialized() && Sentry.captureException(err)
return CustomReportType({message: 'Could not load BonApp report'})
}
}
17 changes: 17 additions & 0 deletions source/reports-bonapp/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {suite, test} from 'node:test'
import assert from 'node:assert/strict'

import * as bonAppReport from './index.js'
import {StavReportType} from './types.js'
import {REPORT_URLS} from '../ccci-stolaf-college/v1/reports.js'

suite('report info', {concurrency: true}, () => {
test('fetching the data should not throw', async () => {
await assert.doesNotReject(() => bonAppReport.report(REPORT_URLS.stav))
})

test('it should return a StavReportType struct', async () => {
let data = await bonAppReport.report(REPORT_URLS.stav)
assert.doesNotThrow(() => StavReportType.parse(data))
})
})
9 changes: 9 additions & 0 deletions source/reports-bonapp/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {z} from 'zod'

export const StavReportType = z.array(
z.object({
title: z.string(),
times: z.array(z.string()),
data: z.array(z.number()),
}),
)

0 comments on commit 49832b9

Please sign in to comment.