This repository has been archived by the owner on May 16, 2020. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #50 from cobalt-uoft/add-textbooks
Add textbooks API
- Loading branch information
Showing
12 changed files
with
1,107 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,7 @@ | |
"files": [ | ||
"test/courses", | ||
"test/buildings", | ||
"test/textbooks", | ||
"test/food" | ||
], | ||
"failFast": true, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, '\\$&') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.