Skip to content
This repository has been archived by the owner on May 16, 2020. It is now read-only.

Commit

Permalink
Merge pull request #50 from cobalt-uoft/add-textbooks
Browse files Browse the repository at this point in the history
Add textbooks API
  • Loading branch information
qasim committed Apr 9, 2016
2 parents 1bc27f0 + f3b8389 commit 085e7e8
Show file tree
Hide file tree
Showing 12 changed files with 1,107 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -46,6 +46,7 @@
"files": [
"test/courses",
"test/buildings",
"test/textbooks",
"test/food"
],
"failFast": true,
Expand Down
7 changes: 7 additions & 0 deletions src/api/textbooks/README.md
@@ -0,0 +1,7 @@
UofT Textbooks API
=================

This is a RESTful web API built to interface with University of Toronto textbooks across all 3 campuses.
It is developed as part of a collection of open data APIs for UofT called [Cobalt](https://cobalt.qas.im).

For more information on how to use Cobalt's APIs, see the [documentation](https://cobalt.qas.im/documentation).
36 changes: 36 additions & 0 deletions src/api/textbooks/index.js
@@ -0,0 +1,36 @@
import express from 'express'
let router = express.Router()

import list from './routes/list'
import show from './routes/show'
import search from './routes/search'
import filter from './routes/filter'

import validation from '../validation'

router.get('/',
validation.limit,
validation.skip,
validation.sort,
list)

router.get('/search',
validation.query,
validation.limit,
validation.skip,
validation.sort,
search)

router.get('/filter',
validation.query,
validation.filterQuery,
validation.limit,
validation.skip,
validation.sort,
filter)

router.get('/:id',
validation.id,
show)

export default router
26 changes: 26 additions & 0 deletions src/api/textbooks/model.js
@@ -0,0 +1,26 @@
import mongoose from 'mongoose'
var Schema = mongoose.Schema

var textbookSchema = new Schema({
id: String,
isbn: String,
title: String,
edition: Number,
author: String,
image: String,
price: Number,
url: String,
courses:[{
id: String,
code: String,
requirement: String,
meeting_sections:[{
code: String,
instructors: [String]
}]
}]
})

textbookSchema.index({ title: 'text' })

export default mongoose.model('textbooks', textbookSchema)
146 changes: 146 additions & 0 deletions src/api/textbooks/routes/filter.js
@@ -0,0 +1,146 @@
import Textbook from '../model'
import co from 'co'

// The absolute (from root) keymap
const ABSOLUTE_KEYMAP = {
'isbn': 'isbn',
'title': 'title',
'edition': 'edition',
'author': 'author',
'image': 'image',
'price': 'price',
'url': 'url',
'course_id': 'courses.id',
'course_code': 'courses.code',
'course_requirement': 'courses.requirement',
'meeting_code': 'courses.meeting_sections.code',
'instructor': 'courses.meeting_sections.instructors'
}

export default function filter(req, res, next) {
let q = req.query.q
q = q.split(' AND ')

let queries = 0

let filter = { $and: q }

for (let i = 0; i < filter.$and.length; i++) {
filter.$and[i] = { $or: q[i].trim().split(' OR ') }

for (let j = 0; j < filter.$and[i].$or.length; j++) {
let part = filter.$and[i].$or[j].trim().split(':')
let x = formatPart(part[0], part[1])

if (x.isValid) {
filter.$and[i].$or[j] = x.query
queries++
}
}
}

if(queries > 0) {
co(function* () {
try {
let docs = yield Textbook
.find(filter, '-__v -_id -courses._id -courses.meeting_sections._id')
.limit(req.query.limit)
.skip(req.query.skip)
.sort(req.query.sort)
.exec()
res.json(docs)
} catch(e) {
return next(e)
}
})
}

}

function formatPart(key, part) {
// Response format
let response = {
key: key,
isValid: true,
query: {}
}

// Checking if the start of the segment is an operator (-, >, <, .>, .<)
if (part.indexOf('-') === 0) {
// Negation
part = {
operator: '-',
value: part.substring(1)
}
} else if (part.indexOf('>=') === 0) {
part = {
operator: '>=',
value: part.substring(2)
}
} else if (part.indexOf('<=') === 0) {
part = {
operator: '<=',
value: part.substring(2)
}
} else if (part.indexOf('>') === 0) {
part = {
operator: '>',
value: part.substring(1)
}
} else if (part.indexOf('<') === 0) {
part = {
operator: '<',
value: part.substring(1)
}
} else {
part = {
operator: undefined,
value: part
}
}

if (isNaN(parseFloat(part.value)) || !isFinite(part.value)) {
// Is not a number
part.value = part.value.substring(1, part.value.length - 1)
} else {
part.value = parseFloat(part.value)
}

if (['edition', 'price'].indexOf(key) > -1) {
// Numbers and arrays of Numbers

if (part.operator === '-') {
response.query[ABSOLUTE_KEYMAP[key]] = { $ne: part.value }
} else if (part.operator === '>') {
response.query[ABSOLUTE_KEYMAP[key]] = { $gt: part.value }
} else if (part.operator === '<') {
response.query[ABSOLUTE_KEYMAP[key]] = { $lt: part.value }
} else if (part.operator === '>=') {
response.query[ABSOLUTE_KEYMAP[key]] = { $gte: part.value }
} else if (part.operator === '<=') {
response.query[ABSOLUTE_KEYMAP[key]] = { $lte: part.value }
} else {
// Assume equality if no operator
response.query[ABSOLUTE_KEYMAP[key]] = part.value
}
} else {
// Strings

if (part.operator === '-') {
response.query[ABSOLUTE_KEYMAP[key]] = {
$regex: '^((?!' + escapeRe(part.value) + ').)*$',
$options: 'i'
}
} else {
response.query[ABSOLUTE_KEYMAP[key]] = {
$regex: '(?i).*' + escapeRe(part.value) + '.*'
}
}
}

return response
}

function escapeRe(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&')
}
18 changes: 18 additions & 0 deletions src/api/textbooks/routes/list.js
@@ -0,0 +1,18 @@
import Textbook from '../model'
import co from 'co'

export default function list(req, res, next) {
co(function* () {
try {
let docs = yield Textbook
.find({}, '-__v -_id -courses._id -courses.meeting_sections._id')
.limit(req.query.limit)
.skip(req.query.skip)
.sort(req.query.sort)
.exec()
res.json(docs)
} catch (e) {
return next(e)
}
})
}
19 changes: 19 additions & 0 deletions src/api/textbooks/routes/search.js
@@ -0,0 +1,19 @@
import Textbook from '../model'
import co from 'co'

export default function search(req, res, next) {
co(function* () {
try {
let docs = yield Textbook
.find({ $text: { $search: req.query.q } },
'-__v -_id -courses._id -courses.meeting_sections._id')
.limit(req.query.limit)
.skip(req.query.skip)
.sort(req.query.sort)
.exec()
res.json(docs)
} catch (e) {
return next(e)
}
})
}
22 changes: 22 additions & 0 deletions src/api/textbooks/routes/show.js
@@ -0,0 +1,22 @@
import Textbook from '../model'
import co from 'co'

export default function show(req, res, next) {
co(function* (){
try {
let doc = yield Textbook
.findOne({ id: req.params.id },
'-__v -_id -courses._id -courses.meeting_sections._id')
.exec()
if (!doc) {
let err = new Error(
'A textbook with the specified identifier does not exist.')
err.status = 400
return next(err)
}
res.json(doc)
} catch (e) {
return next(e)
}
})
}
2 changes: 2 additions & 0 deletions src/db/index.js
Expand Up @@ -49,6 +49,8 @@ db.update = (collection) => {

db.sync = () => {
db.update('buildings')
db.update('food')
db.update('textbooks')
db.update('courses')
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Expand Up @@ -3,6 +3,7 @@ import mongoose from 'mongoose'
import winston from 'winston'
import courses from './api/courses'
import buildings from './api/buildings'
import textbooks from './api/textbooks'
import food from './api/food'
import db from './db'

Expand All @@ -29,6 +30,7 @@ if (!test && enableSync == 'true') {
let apiVersion = '1.0'
app.use(`/${apiVersion}/courses`, courses)
app.use(`/${apiVersion}/buildings`, buildings)
app.use(`/${apiVersion}/textbooks`, textbooks)
app.use(`/${apiVersion}/food`, food)

// Error handlers
Expand Down

0 comments on commit 085e7e8

Please sign in to comment.