Skip to content

Commit 226a4d5

Browse files
authored
Merge pull request #2293 from StoDevX/add-course-search
Add basic course search
2 parents 97f2f05 + df66570 commit 226a4d5

File tree

29 files changed

+1096
-37
lines changed

29 files changed

+1096
-37
lines changed

android/app/src/main/java/com/allaboutolaf/MainActivity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.allaboutolaf;
22

3+
import com.facebook.react.modules.storage.ReactDatabaseSupplier;
34
import com.facebook.react.ReactActivity;
45
import com.bugsnag.BugsnagReactNative;
56
import com.calendarevents.CalendarEventsPackage;
@@ -23,6 +24,8 @@ public void onCreate(Bundle savedInstanceState) {
2324
if (!BuildConfig.DEBUG) {
2425
BugsnagReactNative.start(this);
2526
}
27+
long size = 50L * 1024L * 1024L; // 50 MB
28+
ReactDatabaseSupplier.getInstance(getApplicationContext()).setMaximumSize(size);
2629
}
2730

2831
// Required for react-native-calendar-events

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"react-native-restart": "0.0.6",
115115
"react-native-safari-view": "2.1.0",
116116
"react-native-search-bar": "3.4.0",
117+
"react-native-searchbar": "1.14.0",
117118
"react-native-tableview-simple": "0.17.2",
118119
"react-native-typography": "1.3.0",
119120
"react-native-vector-icons": "4.5.0",
@@ -124,6 +125,7 @@
124125
"redux-promise": "0.5.3",
125126
"redux-thunk": "2.2.0",
126127
"semver": "5.5.0",
128+
"sto-sis-time-parser": "2.3.3",
127129
"stream": "0.0.2",
128130
"timers": "0.1.1",
129131
"titlecase": "1.1.2",

source/flux/parts/sis.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
import {type ReduxState} from '../index'
44
import {getBalances, type BalancesShapeType} from '../../lib/financials'
5+
import {
6+
loadCachedCourses,
7+
updateStoredCourses,
8+
areAnyTermsCached,
9+
} from '../../lib/course-search'
10+
import type {CourseType} from '../../lib/course-search'
511

612
const UPDATE_BALANCES_SUCCESS = 'sis/UPDATE_BALANCES_SUCCESS'
713
const UPDATE_BALANCES_FAILURE = 'sis/UPDATE_BALANCES_FAILURE'
14+
const LOAD_CACHED_COURSES = 'sis/LOAD_CACHED_COURSES'
15+
const COURSES_LOADED = 'sis/COURSES_LOADED'
816

917
type UpdateBalancesSuccessAction = {|
1018
type: 'sis/UPDATE_BALANCES_SUCCESS',
@@ -39,7 +47,51 @@ export function updateBalances(
3947
}
4048
}
4149

42-
type Action = UpdateBalancesActions
50+
type LoadCachedCoursesAction = {|
51+
type: 'sis/LOAD_CACHED_COURSES',
52+
payload: Array<CourseType>,
53+
|}
54+
type CoursesLoadedAction = {|
55+
type: 'sis/COURSES_LOADED',
56+
|}
57+
58+
export type LoadCourseDataActionType = ThunkAction<
59+
LoadCachedCoursesAction | CoursesLoadedAction,
60+
>
61+
export type UpdateCourseDataActionType = ThunkAction<
62+
LoadCachedCoursesAction | CoursesLoadedAction,
63+
>
64+
65+
export function loadCourseDataIntoMemory(): LoadCourseDataActionType {
66+
return async dispatch => {
67+
const areAnyCached = await areAnyTermsCached()
68+
69+
if (!areAnyCached) {
70+
return
71+
}
72+
73+
const cachedCourses = await loadCachedCourses()
74+
dispatch({type: LOAD_CACHED_COURSES, payload: cachedCourses})
75+
dispatch({type: COURSES_LOADED})
76+
}
77+
}
78+
79+
export function updateCourseData(): UpdateCourseDataActionType {
80+
return async dispatch => {
81+
const updateNeeded = await updateStoredCourses()
82+
83+
if (updateNeeded) {
84+
const cachedCourses = await loadCachedCourses()
85+
dispatch({type: LOAD_CACHED_COURSES, payload: cachedCourses})
86+
dispatch({type: COURSES_LOADED})
87+
}
88+
}
89+
}
90+
91+
type Action =
92+
| UpdateBalancesActions
93+
| LoadCachedCoursesAction
94+
| CoursesLoadedAction
4395

4496
export type State = {|
4597
balancesErrorMessage: ?string,
@@ -49,7 +101,10 @@ export type State = {|
49101
mealsRemainingToday: ?string,
50102
mealsRemainingThisWeek: ?string,
51103
mealPlanDescription: ?string,
104+
allCourses: Array<CourseType>,
105+
courseDataState: 'not-loaded' | 'ready',
52106
|}
107+
53108
const initialState = {
54109
balancesErrorMessage: null,
55110
flexBalance: null,
@@ -58,7 +113,10 @@ const initialState = {
58113
mealsRemainingToday: null,
59114
mealsRemainingThisWeek: null,
60115
mealPlanDescription: null,
116+
allCourses: [],
117+
courseDataState: 'not-loaded',
61118
}
119+
62120
export function sis(state: State = initialState, action: Action) {
63121
switch (action.type) {
64122
case UPDATE_BALANCES_FAILURE:
@@ -77,6 +135,11 @@ export function sis(state: State = initialState, action: Action) {
77135
}
78136
}
79137

138+
case LOAD_CACHED_COURSES:
139+
return {...state, allCourses: action.payload}
140+
case COURSES_LOADED:
141+
return {...state, courseDataState: 'ready'}
142+
80143
default:
81144
return state
82145
}

source/lib/course-search/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// @flow
2+
3+
export {loadCachedCourses} from './load-cached-courses'
4+
export {updateStoredCourses, areAnyTermsCached} from './update-course-storage'
5+
export {CourseType, TermType} from './types'
6+
export {parseTerm} from './parse-term'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @flow
2+
3+
import type {CourseType, TermType} from './types'
4+
import flatten from 'lodash/flatten'
5+
import * as storage from '../storage'
6+
7+
export async function loadCachedCourses(): Promise<Array<CourseType>> {
8+
const terms: Array<TermType> = await storage.getTermInfo()
9+
const promises = terms.map(term => storage.getTermCourseData(term.term))
10+
const coursesByTerm = await Promise.all(promises)
11+
return flatten(coursesByTerm)
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// @flow
2+
3+
export function parseTerm(term: string) {
4+
const semester = term.slice(-1)
5+
const year = term.slice(0, -1)
6+
const currentYear = parseInt(year)
7+
const nextYear = (currentYear + 1).toString().slice(-2)
8+
switch (semester) {
9+
case '0':
10+
return `Abroad ${currentYear}/${nextYear}`
11+
case '1':
12+
return `Fall ${currentYear}/${nextYear}`
13+
case '2':
14+
return `Interim ${currentYear}/${nextYear}`
15+
case '3':
16+
return `Spring ${currentYear}/${nextYear}`
17+
case '4':
18+
return `Summer Term 1 ${currentYear}/${nextYear}`
19+
case '5':
20+
return `Summer Term 2 ${currentYear}/${nextYear}`
21+
case '9':
22+
return `Non-St. Olaf ${currentYear}/${nextYear}`
23+
default:
24+
return 'Unknown term'
25+
}
26+
}

source/lib/course-search/types.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @flow
2+
3+
export type CourseType = {
4+
clbid: number,
5+
credits: number,
6+
crsid: number,
7+
departments: string[],
8+
description?: string[],
9+
gereqs?: string[],
10+
instructors: string[],
11+
level: number,
12+
locations?: string[],
13+
name: string,
14+
notes?: string[],
15+
number: number,
16+
pn: boolean,
17+
prerequisites: false | string,
18+
section?: string,
19+
semester: number,
20+
status: string,
21+
term: number,
22+
times?: string[],
23+
title?: string,
24+
type: string,
25+
year: number,
26+
}
27+
28+
export type TermType = {
29+
hash: string,
30+
path: string,
31+
term: number,
32+
type: string,
33+
year: number,
34+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @flow
3+
* updateStoredCourses() handles updating the cached course data from the server
4+
*/
5+
6+
import type {CourseType, TermType} from './types'
7+
import {COURSE_DATA_PAGE, INFO_PAGE} from './urls'
8+
import * as storage from '../storage'
9+
10+
type TermInfoType = {
11+
files: Array<TermType>,
12+
type: string,
13+
}
14+
15+
export async function areAnyTermsCached(): Promise<boolean> {
16+
const localTerms = await storage.getTermInfo()
17+
return localTerms.length === 0 ? false : true
18+
}
19+
20+
export async function updateStoredCourses(): Promise<boolean> {
21+
const outdatedTerms: Array<TermType> = await determineOutdatedTerms()
22+
await Promise.all(outdatedTerms.map(term => storeTermCoursesFromServer(term)))
23+
// returns ``true`` if any terms were updated
24+
return outdatedTerms.length === 0 ? false : true
25+
}
26+
27+
async function determineOutdatedTerms(): Promise<Array<TermType>> {
28+
const remoteTerms: Array<TermType> = await loadCurrentTermsFromServer()
29+
const localTerms: Array<TermType> = await storage.getTermInfo()
30+
if (localTerms.length === 0) {
31+
await storage.setTermInfo(remoteTerms)
32+
return remoteTerms
33+
}
34+
let outdatedTerms = localTerms.filter(localTerm => {
35+
const match = remoteTerms.find(
36+
remoteTerm => remoteTerm.term === localTerm.term,
37+
)
38+
return match ? match.hash !== localTerm.hash : true
39+
})
40+
if (outdatedTerms.length !== 0) {
41+
storage.setTermInfo(remoteTerms)
42+
}
43+
return outdatedTerms
44+
}
45+
46+
async function loadCurrentTermsFromServer(): Promise<Array<TermType>> {
47+
const today = new Date()
48+
const thisYear = today.getFullYear()
49+
const resp: TermInfoType = await fetchJson(INFO_PAGE).catch(() => ({
50+
files: [],
51+
type: 'error',
52+
}))
53+
const terms: Array<TermType> = resp.files.filter(
54+
file => file.type === 'json' && file.year > thisYear - 5,
55+
)
56+
return terms
57+
}
58+
59+
async function storeTermCoursesFromServer(term: TermType) {
60+
const url = COURSE_DATA_PAGE + term.path
61+
const resp: Array<CourseType> = await fetchJson(url).catch(() => [])
62+
storage.setTermCourseData(term.term, resp)
63+
}

source/lib/course-search/urls.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// @flow
2+
3+
export const COURSE_DATA_PAGE = 'https://stodevx.github.io/course-data/'
4+
5+
export const INFO_PAGE = 'https://stodevx.github.io/course-data/info.json'

source/lib/storage.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function removeItem(key: string): Promise<void> {
2727
async function getItemAsBoolean(key: string): Promise<boolean> {
2828
return (await getItem(key)) || false
2929
}
30-
async function getItemAsArray(key: string): Promise<Array<*>> {
30+
async function getItemAsArray<T>(key: string): Promise<Array<T>> {
3131
return (await getItem(key)) || []
3232
}
3333

@@ -74,3 +74,23 @@ export function setFavoriteBuildings(buildings: string[]) {
7474
export function getFavoriteBuildings(): Promise<Array<string>> {
7575
return getItemAsArray(favoriteBuildingsKey)
7676
}
77+
78+
/// MARK: SIS
79+
import type {CourseType, TermType} from './course-search/types'
80+
81+
const courseDataKey = 'sis:course-data'
82+
export function setTermCourseData(term: number, courseData: Array<CourseType>) {
83+
const key = courseDataKey + `:${term}:courses`
84+
return setItem(key, courseData)
85+
}
86+
export function getTermCourseData(term: number): Promise<Array<CourseType>> {
87+
const key = courseDataKey + `:${term}:courses`
88+
return getItemAsArray(key)
89+
}
90+
const termInfoKey = courseDataKey + ':term-info'
91+
export function setTermInfo(termData: Array<TermType>) {
92+
return setItem(termInfoKey, termData)
93+
}
94+
export function getTermInfo(): Promise<Array<TermType>> {
95+
return getItemAsArray(termInfoKey)
96+
}

0 commit comments

Comments
 (0)