Home Identifier Source Repository

src/models/student.js

import Immutable from 'immutable'
import flatten from 'lodash/array/flatten'
import forEach from 'lodash/collection/forEach'
import omit from 'lodash/object/omit'
import uniq from 'lodash/array/uniq'
import {v4 as uuid} from 'node-uuid'
import stringify from 'json-stable-stringify'
import present from 'present'

import {version as currentVersionString} from '../../package.json'
import {studentChangelog as changelog} from '../lib/logger'
import checkGraduatability from '../lib/check-student-graduatability'

import randomChar from '../helpers/random-char'

import Schedule from './schedule'
import Study from './study'

const now = new Date()
const StudentRecord = Immutable.Record({
	id: null,
	name: null,
	version: currentVersionString,

	creditsNeeded: 35,

	matriculation: now.getFullYear() - 2,
	graduation: now.getFullYear() + 2,
	advisor: '',

	dateLastModified: null,
	dateCreated: null,

	studies: Immutable.OrderedMap(),
	schedules: Immutable.OrderedMap(),
	overrides: Immutable.OrderedMap(),
	fabrications: Immutable.OrderedMap(),

	settings: Immutable.Map(),

	graduatability: Promise.resolve({
		canGraduate: false,
		studyResults: Immutable.OrderedMap(),
	}),
})

export default class Student extends StudentRecord {
	constructor(encodedStudent={}) {
		const startTime = present()

		// Don't pass the list params into the StudentRecord constructor;
		// it creates them as JS objects, instead of our custom records.
		const toRemove = ['studies', 'schedules', 'overrides', 'fabrications', 'settings']
		const filtered = omit(encodedStudent, toRemove)
		const immutableStudent = Immutable.fromJS(filtered)

		super(immutableStudent)

		// Then add them manually.
		if (encodedStudent) {
			return this.withMutations(student => {
				student = student.set('id', encodedStudent.id || uuid())
				student = student.set('name', encodedStudent.name || 'Student ' + randomChar())
				student = student.set('dateCreated', encodedStudent.dateCreated || new Date())
				student = student.set('dateLastModified', encodedStudent.dateLastModified || new Date())
				student = student.set('version', currentVersionString)

				forEach((encodedStudent.studies || []), study => {
					student = student.addArea(study)
				})

				forEach((encodedStudent.schedules || []), schedule => {
					student = student.addSchedule(schedule)
				})

				student = student.setOverride(encodedStudent.overrides || {})

				forEach((encodedStudent.fabrications || []), fabrication => {
					student = student.addFabrication(fabrication)
				})

				forEach((encodedStudent.settings || {}), (value, key) => {
					student = student.changeSetting(key, value)
				})

				student.set('graduatability', checkGraduatability(student))

				changelog(`it took ${present() - startTime} ms to make a student`)

				return student
			})
		}
	}

	changeName(newName) {
		return this.set('name', newName)
	}

	changeAdvisor(newAdvisor) {
		return this.set('advisor', newAdvisor)
	}

	changeCreditsNeeded(newCreditsNeeded) {
		return this.set('creditsNeeded', newCreditsNeeded)
	}

	changeMatriculation(newMatriculation) {
		return this.set('matriculation', newMatriculation)
	}

	changeGraduation(newGraduation) {
		return this.set('graduation', newGraduation)
	}

	changeSetting(key, value) {
		return this.setIn(['settings', key], value)
	}

	checkGraduatability() {
		return this.set('graduatability', checkGraduatability(this))
	}


	// schedule methods

	addSchedule(newSchedule) {
		const sched = new Schedule(newSchedule)
		return this.setIn(['schedules', sched.id], sched)
	}

	destroySchedule(scheduleId) {
		changelog(`removing schedule ${scheduleId}`)

		const deadSched = this.getIn(['schedules', scheduleId])
		const scheduleIsNoMore = this.set('schedules', this.schedules.delete(scheduleId))

		if (deadSched && deadSched.active) {
			const otherSchedKey = scheduleIsNoMore.findKey(sched =>
				sched.year === deadSched.year &&
				sched.semester === deadSched.semester &&
				sched.id !== deadSched.id)

			if (otherSchedKey) {
				return scheduleIsNoMore.setIn(['schedules', otherSchedKey, 'active'], true)
			}
		}
		return scheduleIsNoMore
	}

	destroyMultipleSchedules(ids) {
		return this.withMutations(student => {
			changelog('destroyMultipleSchedules', ids)
			ids.forEach(id => {
				student = student.destroySchedule(id)
			})
			return student
		})
	}

	moveCourse(fromScheduleId, toScheduleId, clbid) {
		changelog(`moving course ${clbid} from schedule ${fromScheduleId} to schedule ${toScheduleId}`)
		return this.withMutations(student => {
			student = student.setIn(['schedules', fromScheduleId], student.getIn(['schedules', fromScheduleId]).removeCourse(clbid))
			student = student.setIn(['schedules', toScheduleId], student.getIn(['schedules', toScheduleId]).addCourse(clbid))
		})
	}


	// area-of-study methods

	addArea(areaOfStudy) {
		const study = new Study(areaOfStudy)
		return this.setIn(['studies', study.id], study)
	}

	editArea(areaId, newSource) {
		return this.withMutations(student => {
			let studies = student.studies
			let area = studies.get(areaId)
			area = area.edit(newSource)

			studies = studies.delete(areaId)
			studies = studies.set(area.id, area)

			return student.set('studies', studies)
		})
	}

	removeArea(id) {
		return this.set('studies', this.studies.delete(id))
	}

	removeMultipleAreas(ids) {
		return this.withMutations(student => {
			ids.forEach(id => {
				student = student.removeArea(id)
			})
			return student
		})
	}


	// override methods

	setOverride(overrideObj) {
		return this.withMutations(student => {
			forEach(overrideObj, (val, key) => {
				student = student.setIn(['overrides', key], val)
			})
			return student
		})
	}

	removeOverride(thingOverriden) {
		return this.set('overrides', this.overrides.delete(thingOverriden))
	}


	// fabrication methods

	addFabrication(fabrication) {
		return this.setIn(['fabrications', fabrication.id], fabrication)
	}

	removeFabrication(fabricationId) {
		return this.set('fabrications', this.fabrications.delete(fabricationId))
	}


	// getters

	get courses() {
		// - At it's core, this method just needs to get the list of courses that a student has chosen.
		// - Each schedule has a list of courses that are a part of that schedule.
		// - Additionally, we only care about the schedules that are marked as "active".
		// - Keep in mind that each list of courses is actually a *promise* for the courses.
		// - We also need to make sure to de-duplicate the final list of courses, so that each `clbid` only appears once.
		// - Finally, remember that a given `clbid` might not exist in the database, in which case we get back 'undefined'.
		//   In this case, we need to know where the `clbid` came from, so that we can render an error in the correct location.

		const activeSchedules = this.schedules.filter(s => s.active)
		const promisesForCourses = activeSchedules.map(s => s.courses).toArray()

		return Promise.all(promisesForCourses)
			.then(courses => uniq(flatten(courses), course => course.clbid))
			.catch(err => console.log(err))
	}


	// helpers

	data() {
		return Promise.all([
			this.courses, // 0
			this.creditsNeeded, // 1
			this.fabrications.toList().toJS(), // 2
			this.graduation, // 3
			this.matriculation, // 4
			this.overrides.toObject(), // 5
			this.studies.toList().toJS(), // 6
			this.graduatability, // 7, and 7
		]).then(results => ({
			courses: results[0],
			creditsNeeded: results[1],
			fabrications: results[2],
			graduation: results[3],
			matriculation: results[4],
			overrides: results[5],
			studies: results[6],
			studyResults: results[7].details,
			graduatability: results[7].canGraduate,
		}))
	}

	encode() {
		return encodeURIComponent(stringify(this))
	}

	save() {
		// grab the old (still-JSON-encoded) student from localstorage
		// compare it to the current one
		// if they're different, update dateLastModified, stringify, and save.
		const oldVersion = localStorage.getItem(this.id)

		if (oldVersion !== stringify(this)) {
			changelog(`saving student ${this.name} (${this.id})`)

			const student = this.set('dateLastModified', new Date())
			localStorage.setItem(student.id, stringify(student))
		}
	}

	toJSON() {
		return {
			id: this.id,
			name: this.name,
			version: this.version,
			creditsNeeded: this.creditsNeeded,
			matriculation: this.matriculation,
			graduation: this.graduation,
			studies: this.studies.toList(),
			schedules: this.schedules.toList(),
			overrides: this.overrides,
			fabrications: this.fabrications.toList(),
			settings: this.settings,
		}
	}
}