diff --git a/.gitignore b/.gitignore index 646ac519e..2f9bd06a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store node_modules/ +coverage/ \ No newline at end of file diff --git a/app.js b/app.js index f0579b1dc..65de7aa27 100644 --- a/app.js +++ b/app.js @@ -5,10 +5,13 @@ var logger = require('morgan'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); -var routes = require('./routes/index'); +var massive = require('massive') var app = express(); - +module.exports = app; +var connectionString = "postgres://localhost/cassettecollection_" + app.get('env') +var db = massive.connectSync({connectionString : connectionString}) +app.set("db", db) // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); @@ -21,8 +24,19 @@ app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); + +var routes = require('./routes/index'); app.use('/', routes); +var moviesRoutes = require('./routes/movies'); +app.use('/movies', moviesRoutes); + +var customersRoutes = require('./routes/customers'); +app.use('/customers', customersRoutes); + +var rentalsRoutes = require('./routes/rentals'); +app.use('/rentals', rentalsRoutes); + // catch 404 and forward to error handler app.use(function(req, res, next) { var err = new Error('Not Found'); @@ -54,5 +68,4 @@ app.use(function(err, req, res, next) { }); }); - module.exports = app; diff --git a/controllers/api.js b/controllers/api.js new file mode 100644 index 000000000..fa5a84f03 --- /dev/null +++ b/controllers/api.js @@ -0,0 +1,13 @@ +var docs = require('../docs.json') + +var ApiController = { + docs: function(req, res, next) { + res.render('jadedocs') + }, + + jsonDocs: function(req, res, next) { + res.json(docs) + } +} + +module.exports = ApiController diff --git a/controllers/customers.js b/controllers/customers.js new file mode 100644 index 000000000..d58a91327 --- /dev/null +++ b/controllers/customers.js @@ -0,0 +1,61 @@ +var Customer = require("../models/customer") +var Rental = require('../models/rental') + +var CustomersController = { + index: function(req, res, next) { + Customer.all(function(error, customers) { + if(error) { + var err = new Error("Error retrieving customer list:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(customers) + } + }); + }, + + sortBy: function(req, res, next) { + var type = req.params.query + var n = req.query.n + var p = req.query.p + var firstrow = n*(p-1)+1 + var lastrow = n*p + Customer.sortBy([type,firstrow,lastrow],function(error, customers) { + if(error) { + var err = new Error("Error retrieving customer list:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(customers) + } + }); + }, + + current: function(req, res, next) { + var id = req.params.id + Rental.currentCheckedOut([id,'checked_out'], function(error, customers) { + if(error) { + var err = new Error("Error retrieving customer list:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(customers) + } + }); + }, + + history: function (req, res, next) { + var id = req.params.id + Rental.all([id], function(error, customers) { + if(error) { + var err = new Error("Error retrieving customer list:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(customers) + } + }); + } +} + +module.exports = CustomersController diff --git a/controllers/movies.js b/controllers/movies.js new file mode 100644 index 000000000..392f38987 --- /dev/null +++ b/controllers/movies.js @@ -0,0 +1,61 @@ +var Movie = require("../models/movie") +var Customer = require("../models/customer") + +var MoviesController = { + index: function (req, res, next) { + Movie.all(function(error, movies) { + if (error) { + var err = new Error("Error retrieving customer list:\n" + error.message) + err.status = 500 + next(err) + } else { + res.json(movies) + } + }) + }, + + sortBy: function (req, res, next) { + var type = req.params.query + var n = req.query.n + var p = req.query.p + var firstrow = n * (p - 1) + 1 + var lastrow = n * p + Movie.sortBy([type, firstrow, lastrow], function (error, movies) { + if (error) { + var err = new Error("Error retrieving customer list:\n" + error.message) + err.status = 500 + next(err) + } else { + res.json(movies) + } + })}, + + current: function (req, res, next) { + var title = req.params.movie + Customer.customersWithMovie([title], function (error, movies) { + if (error) { + var err = new Error("Error retrieving customer list:\n" + error.message) + err.status = 500 + next(err) + } else { + res.json(movies) + } + })}, + + sortedHistory: function (req, res, next) { + var title = req.params.movie + var sort = req.params.by + Customer.rentedThisMovie([title, sort], function (error, movies) { + if (error) { + var err = new Error("Error retrieving customer list:\n" + error.message) + err.status = 500 + next(err) + } else { + res.json(movies) + } + })} + + +} + +module.exports = MoviesController diff --git a/controllers/rentals.js b/controllers/rentals.js new file mode 100644 index 000000000..5cd7243fd --- /dev/null +++ b/controllers/rentals.js @@ -0,0 +1,81 @@ +var Rental = require("../models/rental") +var Movie = require("../models/movie") +var Customer = require("../models/customer") + +var RentalsController = { + + lookupMovie: function (req, res, next) { + var title = req.params.movie + + Movie.rentalInfo([title], function (error, rentals) { + if(error) { + var err = new Error("Error retrieving movie info:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(rentals) + } + })}, + + currentlyCheckedOut: function (req, res, next) { + var title = req.params.movie + + Customer.currentlyCheckedOut([title],function (error, customers) { + if(error) { + var err = new Error("Error retrieving overdue list:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(customers) + } + }); + }, + + checkOut: function (req, res, next) { + var title = req.params.movie + var id = 55 + // need to test to see if customer's id comes in body + // if we don't want to have them passed in the body we can alter the url to take a + // second parameter ->/rentals/:movie/check-out/:cust_id + + Rental.checkOut([title, id],function (error, rental) { + if(error) { + var err = new Error("Error checking out rental:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(rental) + } + }); + }, + + return: function (req, res, next) { + var title = req.params.movie + var id = 55 + // need to test to see if customer's id comes in body + + Rental.return([title, id],function (error, rental) { + if(error) { + var err = new Error("Error checking out rental:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(rental) + } + }); + }, + + overdue: function (req, res, next) { + Rental.overdueList(function (error, rentals) { + if(error) { + var err = new Error("Error retrieving overdue list:\n" + error.message); + err.status = 500; + next(err); + } else { + res.json(rentals) + } + }); + } +} + +module.exports = RentalsController diff --git a/db/seeds/rentals.json b/db/seeds/rentals.json new file mode 100644 index 000000000..287d49e73 --- /dev/null +++ b/db/seeds/rentals.json @@ -0,0 +1,44 @@ +[ +{ +"movie_id": 2, +"customer_id": 4, +"status": "checked_out", +"checkout_date": "2016-06-14", +"due_date": "2016-06-24" +}, +{ +"movie_id": 4, +"customer_id": 4, +"status": "returned", +"checkout_date": "1960-06-03", +"due_date": "1960-06-13" +}, +{ +"movie_id": 1, +"customer_id": 4, +"status": "checked_out", +"checkout_date": "2016-06-03", +"due_date": "2017-06-13" +}, +{ +"movie_id": 33, +"customer_id": 55, +"status": "overdue", +"checkout_date": "1960-06-03", +"due_date": "1960-06-13" +}, +{ +"movie_id": 1, +"customer_id": 3, +"status": "returned", +"checkout_date": "2016-06-02", +"due_date": "2016-06-12" +}, +{ +"movie_id": 1, +"customer_id": 4, +"status": "returned", +"checkout_date": "1960-06-03", +"due_date": "1960-06-13" +} +] diff --git a/db/setup/schema.sql b/db/setup/schema.sql new file mode 100644 index 000000000..2542773be --- /dev/null +++ b/db/setup/schema.sql @@ -0,0 +1,41 @@ +DROP TABLE IF EXISTS movies; +CREATE TABLE movies( + id serial PRIMARY KEY, + title text, + overview text, + release_date text, + inventory integer +); + +CREATE INDEX movies_title ON movies (title); +CREATE INDEX movies_date ON movies (release_date); + +DROP TABLE IF EXISTS customers; +CREATE TABLE customers( + id serial PRIMARY KEY, + name text, + registered_at text, + address text, + city text, + state text, + postal_code text, + phone text, + account_credit decimal +); + +CREATE INDEX customers_name ON customers (name); +CREATE INDEX customers_date ON customers (registered_at); +CREATE INDEX customers_postal ON customers (postal_code); + +DROP TABLE IF EXISTS rentals; +CREATE TABLE rentals( + id serial PRIMARY KEY, + movie_id integer REFERENCES movies (id), + customer_id integer REFERENCES customers (id), + status text, + checkout_date text, + due_date text +); + +CREATE INDEX rentals_customers ON rentals (customer_id); +CREATE INDEX rentals_status ON rentals (status); diff --git a/db/setup/seed.sql b/db/setup/seed.sql new file mode 100644 index 000000000..e69de29bb diff --git a/docs.json b/docs.json new file mode 100644 index 000000000..9e352e444 --- /dev/null +++ b/docs.json @@ -0,0 +1,50 @@ +[ + { + "CassetteCollection": "The Tunes & Takeout API allows programmatic access to randomized suggestions of specific food and music pairings, that data for which is obtained from the Yelp and Spotify APIs. The API endpoint is available at https://tunes-takeout-api.herokuapp.com/ and all paths listed in this documentation should be assumed to use that base URL." + }, + { + "Customers": [ + { + "Request": "This is version 1 of the Tunes & Takeout API. To ensure consistency if the API evolves further, all requests to version 1 are prefixed with the path /v1/." + }, + { + "Response": "Search results are JSON documents containing a list of suggestion hashes, and a canonical URL for the request itself. Each suggestion hash includes the ID for a specific business from the Yelp API as well as an ID and type for an item from the Spotify API." + }, + { + "Example": [ + { + "Request URL": "/customers" + }, + { + "Response data": [ + { + "id": 1, + "name": "Shelley Rocha", + "registered_at": "Wed, 29 Apr 2015 07:54:14 -0700", + "postal_code": "24309", + "phone": "(322) 510-8695", + "account_credit": "13.15" + }, + { + "id": 2, + "name": "Curran Stout", + "registered_at": "Wed, 16 Apr 2014 21:40:20 -0700", + "postal_code": "94267", + "phone": "(908) 949-6758", + "account_credit": "35.66" + }, + { + "id": 3, + "name": "Roanna Robinson", + "registered_at": "Fri, 28 Nov 2014 13:14:08 -0800", + "postal_code": "15867", + "phone": "(323) 336-1841", + "account_credit": "50.39" + } + ]} + ] + } + + + ]} +] diff --git a/models/customer.js b/models/customer.js new file mode 100644 index 000000000..823445f15 --- /dev/null +++ b/models/customer.js @@ -0,0 +1,87 @@ +var app = require("../app"); +var db = app.get("db"); + +// Constructor function +var Customer = function(customer) { + this.id = customer.id; + this.name = customer.name; + this.registered_at = customer.registered_at; + this.postal_code = customer.postal_code; + this.phone = customer.phone; + this.account_credit = customer.account_credit; + if (customer.checkout_date) { + this.checkout_date = customer.checkout_date + } + if (customer.due_date) { + this.due_date = customer.due_date + } +}; + + +Customer.all = function(callback) { + db.run("SELECT * FROM customers;", function(error, customers) { + if(error || !customers) { + callback(error || new Error("Could not retrieve Customers"), undefined); + } else { + callback(null, customers.map(function(customer) { + return new Customer(customer); + })); + } + }); +}; + +Customer.sortBy = function(input, callback){ + var order = input.shift() + db.run("Select * From (Select Row_Number() Over (Order By " + order + ") As RowNum, *From customers) customers Where RowNum BETWEEN $1 AND $2;",input, function(error, customers) { + if(error || !customers) { + callback(error || new Error("Could not retrieve Customers"), undefined); + } else { + callback(null, customers.map(function(customer) { + return new Customer(customer); + })); + } + }); +}; + +Customer.customersWithMovie = function(input, callback){ + db.run("SELECT * FROM (SELECT customer_id FROM rentals WHERE (SELECT id FROM movies WHERE movies.title = $1 and status='checked_out') = movie_id) as new_ids INNER JOIN customers ON (new_ids.customer_id = customers.id);",input, function(error, customers) { + if(error || !customers) { + callback(error || new Error("Could not retrieve Customers"), undefined); + } else { + callback(null, customers.map(function(customer) { + return new Customer(customer); + })); + } + }); +}; + +Customer.rentedThisMovie = function(input, callback) { + var order = input.pop() + + db.run("SELECT * FROM (SELECT customer_id, checkout_date FROM rentals WHERE (SELECT id FROM movies WHERE movies.title = $1) = movie_id) as new_ids JOIN customers ON (new_ids.customer_id = customers.id) ORDER BY " + order + ";",input, function(error, customers) { + if(error || !customers) { + callback(error || new Error("Could not retrieve Customers"), undefined); + } else { + callback(null, customers.map(function(customer) { + return new Customer(customer); + })); + } + }); +}; + +Customer.currentlyCheckedOut = function (input,callback) { + // var order = input.shift() + db.run("SELECT customers.name, checkout_date, due_date, movies.title FROM (SELECT customer_id, due_date, checkout_date, movie_id FROM rentals WHERE status='checked_out' OR status='overdue') as checkout INNER JOIN customers ON (checkout.customer_id = customers.id) INNER JOIN movies ON (checkout.movie_id = movies.id) WHERE movies.title=$1;",input, function (error, customers) { + if(error || !customers) { + callback(error || new Error("Could not retrieve rentals"), undefined); + } else { + callback(null, customers.map(function (customer) { + return new Customer(customer); + })); + } + }); +} + + + +module.exports = Customer diff --git a/models/movie.js b/models/movie.js new file mode 100644 index 000000000..c8ac29229 --- /dev/null +++ b/models/movie.js @@ -0,0 +1,55 @@ +var app = require("../app"); +var db = app.get("db"); + +// Constructor function +var Movie = function(movie) { + this.id = movie.id; + this.title = movie.title; + this.release = movie.release_date; + this.overview = movie.overview; + this.inventory = movie.inventory; + if (movie.available_inventory) { + this.available_inventory = movie.available_inventory + } +}; + + +Movie.all = function(callback) { + db.run("SELECT * FROM movies;", function(error, movies) { + if(error || !movies) { + callback(error || new Error("Could not retrieve movies"), undefined); + } else { + callback(null, movies.map(function(movie) { + return new Movie(movie); + })); + } + }); +}; + +Movie.sortBy = function(input, callback){ + var order = input.shift() + db.run("Select * From (Select Row_Number() Over (Order By " + order + ") As RowNum, *From movies) movies Where RowNum BETWEEN $1 AND $2;",input, function(error, movies) { + // console.log("luff ", movies) + if(error || !movies) { + callback(error || new Error("Could not retrieve Movies"), undefined); + } else { + callback(null, movies.map(function(movie) { + return new Movie(movie); + })); + } + }); +}; + +Movie.rentalInfo = function(input, callback) { + db.run("SELECT title, overview, release_date, inventory, (inventory - (SELECT count(*)FROM rentals WHERE (SELECT id FROM movies WHERE movies.title = $1) = movie_id AND status='checked_out')) as available_inventory FROM movies WHERE title = $1;",input, function(error, movie) { + if(error || !movie) { + callback(error || new Error("Could not retrieve movie info"), undefined); + } else { + callback(null, movie.map(function(movie) { + return new Movie(movie); + })); + } + }); +}; + +module.exports = Movie diff --git a/models/rental.js b/models/rental.js new file mode 100644 index 000000000..7fa0322f6 --- /dev/null +++ b/models/rental.js @@ -0,0 +1,141 @@ +var app = require("../app"); +var db = app.get("db"); + +// Constructor function +var Rental = function(rental) { + this.title = rental.title; + this.id = rental.id; + this.checkout_date = rental.checkout_date; + this.due_date = rental.due_date; + this.status = rental.status + this.name = rental.name +}; + +Rental.currentCheckedOut = function(input,callback){ + // var order = input.shift() + db.run("select * from (select * from rentals,movies where rentals.movie_id=movies.id) as joined where customer_id=$1 and status=$2;",input, function(error, rentals) { + if(error || !rentals) { + callback(error || new Error("Could not retrieve rentals"), undefined); + } else { + callback(null, rentals.map(function(rental) { + return new Rental(rental); + })); + } + }); +} + +Rental.all = function (input,callback) { + // var order = input.shift() + db.run("select * from (select * from rentals,movies where rentals.movie_id=movies.id) as joined where customer_id=$1 order by due_date;", input, function (error, rentals) { + if(error || !rentals) { + callback(error || new Error("Could not retrieve rentals"), undefined); + } else { + callback(null, rentals.map(function (rental) { + return new Rental(rental); + })); + } + }); +} + +Rental.overdueList = function (callback) { + db.run("SELECT customers.name, movies.title, checkout_date, due_date FROM (SELECT customer_id, movie_id, due_date, checkout_date FROM rentals WHERE status='overdue') as overdues INNER JOIN customers ON (overdues.customer_id = customers.id) INNER JOIN movies ON (overdues.movie_id = movies.id);", function (error, rentals) { + if(error || !rentals) { + callback(error || new Error("Could not retrieve rentals"), undefined); + } else { + callback(null, rentals.map(function (rental) { + return new Rental(rental); + })); + } + }); +} + +Rental.checkOut = function (input, callback) { + db.movies.findOne({title: input[0]}, function (error, found_id) { + db.run("UPDATE movies SET inventory=inventory-1 WHERE title=$1;", [found_id.title], function (error, meh) {}) + db.rentals.save({customer_id: input[1], movie_id: found_id.id, due_date: due(), checkout_date: today(), status: "checked_out"}, function (error, rentals) { + rentals = [rentals] + if(error || !rentals) { + callback(error || new Error("Could not retrieve rentals"), undefined); + } else { + callback(null, rentals.map(function (rental) { + return new Rental(rental); + })) + } + }) + }) +} + +Rental.return = function (input, callback) { + db.movies.findOne({title: input[0]}, function (error, found_id) { + db.run("UPDATE movies SET inventory=inventory+1 WHERE title=$1;", [found_id.title], function (error, meh) {}) + + db.rentals.findOne({movie_id: found_id.id, customer_id: input[1], status: "checked_out"}, function (error, rental) { + db.rentals.update({id: rental.id, status: "returned"}, function (error, rentals) { + rentals = [rentals] + if(error || !rentals) { + callback(error || new Error("Could not retrieve rentals"), undefined); + } else { + callback(null, rentals.map(function (rental) { + return new Rental(rental); + })) + } + }) + }) + }) +} + +due = function () { + date = new Date(Date.now() + 1000000000) // uhh magic number.. adds 12 days + + yr = date.getFullYear().toString() + mo = (date.getMonth()+1).toString() + mo = mo[1] ? mo : "0" + mo[0] + da = date.getDate().toString() + da = da[1] ? da : "0" + da[0] + fdate = yr + "-" + mo + "-" + da + return fdate + +} + +today = function () { + date = new Date(Date.now()) + + yr = date.getFullYear().toString() + mo = (date.getMonth()+1).toString() + mo = mo[1] ? mo : "0" + mo[0] + da = date.getDate().toString() + da = da[1] ? da : "0" + da[0] + fdate = yr + "-" + mo + "-" + da + return fdate +} + +// Rental.overdueList = function (input, callback) { +// db.run("SELECT id FROM movies WHERE title=$1;", input, db.rentals.save(error , function (error, rentals) { +// console.log("halp") +// if(error || !rentals) { +// callback(error || new Error("Could not retrieve rentals"), undefined); +// } else { +// callback(null, rentals.map(function (rental) { +// return new Rental(rental); +// })); +// } +// })) +// } + + + + + +// Rental.all = function (input, callback) { +// var order = input.shift() +// db.run("SELECT * FROM (SELECT customer_id, checkout_date FROM rentals WHERE (SELECT id FROM movies WHERE movies.title = $1) = movie_id) as new_ids JOIN customers ON (new_ids.customer_id = customers.id) ORDER BY " + order + ";", input, function (error, rentals) { +// if(error || !rentals) { +// callback(error || new Error("Could not retrieve rentals"), undefined); +// } else { +// callback(null, rentals.map(function (rental) { +// return new Rental(rental); +// })); +// } +// }); +// } +module.exports = Rental diff --git a/package.json b/package.json index d39b26403..7cd660785 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,30 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "nodemon ./bin/www", - "test": "clear; jasmine-node --verbose spec/" + "start": "./node_modules/.bin/nodemon ./bin/www", + "start-test": "NODE_ENV=test ./node_modules/.bin/nodemon ./bin/www", + "test": "clear; ./node_modules/.bin/istanbul cover -x 'spec/**/*' -- ./node_modules/.bin/jasmine-node --captureExceptions --verbose spec/", + "db:drop": "dropdb cassettecollection_development && dropdb cassettecollection_test", + "db:create": "createdb cassettecollection_development; createdb cassettecollection_test", + "db:schema": "node tasks/load_schema.js", + "db:seed": "node tasks/seed_data.js", + "db:reset": "npm run db:drop; npm run db:create; npm run db:schema; npm run db:seed", + "coverage": "open coverage/lcov-report/index.html" }, "dependencies": { "body-parser": "~1.13.2", "cookie-parser": "~1.3.5", "debug": "~2.2.0", + "ejs": "^2.4.2", "express": "~4.13.1", "jade": "~1.11.0", + "massive": "^2.3.0", "morgan": "~1.6.1", "sequelize": "^3.23.3", "serve-favicon": "~2.3.0" }, "devDependencies": { + "istanbul": "^0.4.4", "jasmine-node": "^1.14.5", "nodemon": "^1.9.2", "request": "^2.72.0" diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 9453385b9..b408a4841 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -1,8 +1,36 @@ body { - padding: 50px; - font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; + padding: 0 50px 50px 50px; + background-color: #f8f9f8; + font-size: : 16px; + font-family: 'Open Sans', sans-serif; +} + +h1, h2, h3, h4 { + font-family: 'Gloria Hallelujah', cursive; +} + +h1 { + font-size: 4rem; } a { color: #00B7FF; + text-decoration: none; +} + +.docs-container { + width: 70%; + margin: auto; + /*text-align: center;*/ +} + +.docs-header h1 { + margin: 0; + text-align: center; +} + +.docs-header img { + display: block; + width: 60%; + margin: auto; } diff --git a/routes/customers.js b/routes/customers.js new file mode 100644 index 000000000..dca9a57db --- /dev/null +++ b/routes/customers.js @@ -0,0 +1,10 @@ +var express = require('express'); +var router = express.Router(); +var CustomersController = require('../controllers/customers') + +router.get('/', CustomersController.index) // nothing here, probably omit +router.get('/sort/:query', CustomersController.sortBy) +router.get('/:id/current', CustomersController.current) +router.get('/:id/history', CustomersController.history) + +module.exports = router; diff --git a/routes/index.js b/routes/index.js index 06cfc1137..b7aeb91ba 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,9 +1,21 @@ var express = require('express'); var router = express.Router(); +var ApiController = require('../controllers/api') + /* GET home page. */ router.get('/', function(req, res, next) { res.status(200).json({whatevs: 'whatevs!!!'}) }); +router.get('/zomg', function(req, res, next) { + res.status(200).json({message: 'it works!'}) +}); + +router.get('/api/docs', ApiController.docs); + +router.get('/api/docs.json', ApiController.jsonDocs); + + + module.exports = router; diff --git a/routes/movies.js b/routes/movies.js new file mode 100644 index 000000000..61894b354 --- /dev/null +++ b/routes/movies.js @@ -0,0 +1,11 @@ +var express = require('express'); +var router = express.Router(); +var MoviesController = require('../controllers/movies') + +router.get('/', MoviesController.index) +router.get('/sort/:query', MoviesController.sortBy) +router.get('/:movie/current', MoviesController.current) +router.get('/:movie/history/sort/:by', MoviesController.sortedHistory) + + +module.exports = router; diff --git a/routes/rentals.js b/routes/rentals.js new file mode 100644 index 000000000..19cc793db --- /dev/null +++ b/routes/rentals.js @@ -0,0 +1,11 @@ +var express = require('express'); +var router = express.Router(); +var RentalsController = require('../controllers/rentals') + +router.get('/overdue', RentalsController.overdue) +router.post('/:movie/check-out', RentalsController.checkOut) // requires json data customer_id and movie title +router.get('/:movie/customers', RentalsController.currentlyCheckedOut) // do we need to pass stuff in? +router.put('/:movie/return', RentalsController.return) // requires json data customer_id and movie title +router.get('/:movie', RentalsController.lookupMovie) + +module.exports = router; diff --git a/spec/controllers/customers.spec.js b/spec/controllers/customers.spec.js new file mode 100644 index 000000000..df88d9c81 --- /dev/null +++ b/spec/controllers/customers.spec.js @@ -0,0 +1,94 @@ +var request = require('request') +var app = require("../../app.js") +var base_url = "http://localhost:3000/customers" + +describe("CustomersController", function(){ + + describe("#index", function(done){ + it("returns a success response", function(done){ + request.get(base_url, function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url, function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + + it("returns customer information", function(done){ + request.get(base_url, function(error, response, body){ + var data = JSON.parse(body) + expect(Object.keys(data[4])).toEqual(["id", "name", "registered_at", "postal_code", "phone", "account_credit"]) + done() + }) + }) + }) + + describe("#sortBy", function(done){ + it("returns a success response", function(done){ + request.get(base_url+"/sort/name?n=10&p=2", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url+"/sort/name?n=10&p=2", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + + it("returns customer information", function(done){ + request.get(base_url+"/sort/name?n=10&p=2", function(error, response, body){ + var data = JSON.parse(body) + expect(Object.keys(data[0])).toEqual(["id", "name", "registered_at", "postal_code", "phone", "account_credit"]) + done() + }) + }) + + it("returns the correct number of rows", function(done){ + request.get(base_url+"/sort/name?n=22&p=2", function(error, response, body){ + var data = JSON.parse(body) + expect(data.length).toEqual(22) + done() + }) + }) + }) + + describe("#current", function(done){ + it("returns a success response", function(done){ + request.get(base_url+"/1/current", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url+"/1/current", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) + + describe("#history", function(done){ + it("returns a success response", function(done){ + request.get(base_url+"/33/history", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url+"/33/history", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) +}) diff --git a/spec/controllers/movies.spec.js b/spec/controllers/movies.spec.js index ddcaf2f68..049757f3b 100644 --- a/spec/controllers/movies.spec.js +++ b/spec/controllers/movies.spec.js @@ -1,5 +1,91 @@ -var request = require('request'); +var request = require('request') +var app = require("../../app.js") +var base_url = "http://localhost:3000/movies" + +describe("RentalsController", function(){ + + describe("#index", function(done){ + it("returns a success response", function(done){ + request.get(base_url, function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url, function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) + + it('returns a body of content', function (done) { + request.get(base_url, function(error, response, body) { + expect(body).toNotBe(null) + done() + }) + }) + + it('returns json', function (done) { + request.get(base_url, function(error, response, body) { + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + + describe('/movies/sort/', function () { + var goodSort = '/sort/release_date?n=5&p=1' + var badSort = '/sort/year?n=5&p1' + + it('responds with 200 for a good request', function (done) { + request.get(base_url + goodSort, function(error, response, body) { + expect(response.statusCode).toEqual(200) + done() + }) + }) + + it('returns json', function (done) { + request.get(base_url + goodSort, function(error, response, body) { + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + + it('responds with 500 for a bad request', function (done) { + request.get(base_url + badSort, function(error, response, body) { + expect(response.statusCode).toEqual(500) + done() + }) + }) + + }) + + describe('/movies/:title/current', function () { + var goodCurrent = '/Psycho/current' + var badCurrent = '/Young%20Frankenstein/current' + + it('responds with 200 for a good request', function (done) { + request.get(base_url + goodCurrent, function(error, response, body) { + expect(response.statusCode).toEqual(200) + done() + }) + }) + + it('returns json', function (done) { + request.get(base_url + goodCurrent, function(error, response, body) { + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + + it('responds with empty array for bad request', function (done) { + request.get(base_url + badCurrent, function(error, response, body) { + expect(body).toEqual('[]') + done() + }) + }) + + }) -describe("Endpoints under /movies", function() { - }) diff --git a/spec/controllers/rentals.spec.js b/spec/controllers/rentals.spec.js new file mode 100644 index 000000000..852dc3ba0 --- /dev/null +++ b/spec/controllers/rentals.spec.js @@ -0,0 +1,86 @@ +var request = require('request') +var app = require("../../app.js") +var base_url = "http://localhost:3000/rentals" + +describe("RentalsController", function(){ + + describe("#overdue", function(done){ + it("returns a success response", function(done){ + request.get(base_url+"/overdue", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url+"/overdue", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) + + describe("#lookupMovie", function(done){ + it("returns a success response", function(done){ + request.get(base_url+"/Jaws", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url+"/Jaws", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) + + describe("#currentlyCheckedOut", function(done){ + it("returns a success response", function(done){ + request.get(base_url+"/Jaws/customers", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.get(base_url+"/Psycho/customers", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) + + describe("#checkout", function(done){ + it("returns a success response", function(done){ + request.post(base_url+"/Jaws/check-out", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.post(base_url+"/Psycho/check-out", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) + + describe("#return", function(done){ + it("returns a success response", function(done){ + request.put(base_url+"/Jaws/return", function(error, response, body){ + expect(response.statusCode).toBe(200) + done() + }) + }) + + it("returns json", function(done){ + request.put(base_url+"/Psycho/return", function(error, response, body){ + expect(response.headers['content-type']).toContain('application/json') + done() + }) + }) + }) +}) \ No newline at end of file diff --git a/spec/models/customer.spec.js b/spec/models/customer.spec.js new file mode 100644 index 000000000..4b2c4efb0 --- /dev/null +++ b/spec/models/customer.spec.js @@ -0,0 +1,118 @@ +var app = require('../../app') +var db = app.get('db') + +var Customer = require('../../models/customer') + +describe('Customer', function () { + beforeEach(function(){ + + }) + + afterEach(function () { + db.end() + }) + + describe('.all', function () { + it('should return an array', function(done) { + Customer.all(function(error,customers){ + expect(customers).toEqual(jasmine.any(Array)) + done() + }) + + }) + + it('be at least certain size', function(done) { + Customer.all(function(error,customers){ + expect(customers.length).toBeGreaterThan(199) + done() + }) + + }) + + }) + + describe('.sortBy', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Customer.sortBy(['postal_code',1,20],function(error,customers){ + expect(customers).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Customer.sortBy(['name',1,20], function(error,customers){ + expect(customers.length).toEqual(20) + done() + }) + }) + + it('produces an error when it doesnt recognize a column', function(done) { + Customer.sortBy(['imanerror',1,20], function(error,customers){ + expect(error.message).toEqual('column "imanerror" does not exist') + done() + }) + }) + }) + + describe('.customersWithMovie', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Customer.customersWithMovie(['Jaws'],function(error,customers){ + expect(customers).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Customer.customersWithMovie(['Psycho'], function(error,customers){ + expect(customers.length).toEqual(1) + done() + }) + }) + + it('returns an error if no input', function(done) { + Customer.customersWithMovie([], function(error,customers){ + expect(customers).toEqual(undefined) + expect(error).toNotEqual(null) + done() + }) + }) + }) + + describe('.rentedThisMovie', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Customer.rentedThisMovie(['Jaws', 'checkout_date'], function(error,customers){ + expect(customers).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Customer.rentedThisMovie(['Psycho', 'name'], function(error,customers){ + expect(customers.length).toBeGreaterThan(2) + done() + }) + }) + }) + + describe('.currentlyCheckedOut', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Customer.currentlyCheckedOut(['Jaws'], function(error,customers){ + expect(customers).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Customer.currentlyCheckedOut(['Psycho'], function(error,customers){ + expect(customers.length).toEqual(1) + done() + }) + }) + }) +}) + + diff --git a/spec/models/movie.spec.js b/spec/models/movie.spec.js new file mode 100644 index 000000000..5a0d755f9 --- /dev/null +++ b/spec/models/movie.spec.js @@ -0,0 +1,124 @@ +var app = require('../../app') +var db = app.get('db') + +var Movie = require('../../models/movie') + +describe('Movie', function () { + var title = 'High Noon' + var overview = 'High Noon is about a recently freed leader of a gang of bandits in the desert who is looking to get revenge on the Sheriff who put him in jail. A legendary western film from the Austrian director Fred Zinnemann.' + var release_date = '1952-07-24' + var inventory = 4 + + afterEach(function () { + db.end() + }) + + describe('all', function () { + it('should not be null', function (done) { + Movie.all(function (error, movies) { + expect(movies).toNotBe(null) + done() + }) + }) + + it('should return an array', function (done) { + Movie.all(function (error, movies) { + expect(movies).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('should contain movie objects', function (done) { + Movie.all(function (error, movies) { + expect(movies[0]).toEqual(jasmine.any(Movie)) + done() + }) + }) + + it('should contain all movies that exist in the database', function (done) { + Movie.all(function (error, movies) { + expect(movies.length).toEqual(100) + done() + }) + }) + }) + + describe('sortBy', function () { + var firstAlphabeticalMovieTitle = "12 Angry Men" + var firstReleaseDate = "1923-04-01" + + it('should return an array', function (done) { + Movie.sortBy(['title', 1, 10], function (error, movies) { + expect(movies).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('should contain movie objects', function (done) { + Movie.sortBy(['title', 2, 12], function (error, movies) { + expect(movies[0]).toEqual(jasmine.any(Movie)) + done() + }) + }) + + it('should be able to sort by name', function (done) { + Movie.sortBy(['title', 1, 25], function (error, movies) { + expect(movies[0].title).toEqual(firstAlphabeticalMovieTitle) + done() + }) + }) + + it('should be able to sort by release_date', function (done) { + Movie.sortBy(['release_date', 1, 15], function (error, movies) { + expect(movies[0].release).toEqual(firstReleaseDate) + done() + }) + }) + + it('returns an empty array when the given an invalid sort type', function (done) { + Movie.sortBy(['year', 3, 15], function (error, movies) { + expect(movies).toEqual(undefined) + done() + }) + }) + }) + + describe('rentalInfo', function () { + + it('should return an array', function (done) { + Movie.rentalInfo([title], function (error, movies) { + expect(movies).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('should contain movie objects', function (done) { + Movie.rentalInfo([title], function (error, movies) { + expect(movies[0]).toEqual(jasmine.any(Movie)) + done() + }) + }) + + it('should find the correct movie', function (done) { + Movie.rentalInfo(["Psycho"], function (error, movies) { + expect(movies[0].title).toEqual("Psycho") + done() + }) + }) + + it('should only return one movie', function (done) { + Movie.rentalInfo([title], function (error, movies) { + expect(movies.length).toEqual(1) + done() + }) + }) + + it('returns an empty array when it does not find the movie title', function (done) { + Movie.rentalInfo(["Mr. Nobody"], function (error, movies) { + expect(movies).toEqual([]) + done() + }) + }) + }) + +}) diff --git a/spec/models/rental.spec.js b/spec/models/rental.spec.js new file mode 100644 index 000000000..7acfd26af --- /dev/null +++ b/spec/models/rental.spec.js @@ -0,0 +1,91 @@ +var app = require('../../app') +var db = app.get('db') + +var Rental = require('../../models/rental') + +describe('Rental', function () { + + afterEach(function () { + db.end() + }) + + describe('.currentCheckedOut', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Rental.currentCheckedOut([55,'checked_out'], function(error,rentals){ + expect(rentals).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Rental.currentCheckedOut([4,'checked_out'], function(error,rentals){ + expect(rentals.length).toBeGreaterThan(1) + done() + }) + }) + }) + + describe('.all', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Rental.all([55], function(error,rentals){ + expect(rentals).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Rental.all([4], function(error,rentals){ + expect(rentals.length).toBeGreaterThan(3) + done() + }) + }) + + it('returns an error if no input', function(done) { + Rental.all([], function(error,rentals){ + // console.log(error, rentals) + expect(rentals).toEqual(undefined) + expect(error).toNotEqual(null) + done() + }) + }) + }) + + describe('.overdueList', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Rental.overdueList(function(error,rentals){ + expect(rentals).toEqual(jasmine.any(Array)) + done() + }) + }) + + it('be a certain size', function(done) { + Rental.overdueList(function(error,rentals){ + expect(rentals.length).toEqual(1) + done() + }) + }) + }) + + describe('.checkOut', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Rental.checkOut(['Jaws', 4], function(error,rentals){ + expect(rentals).toEqual(jasmine.any(Array)) + done() + }) + }) + }) + + describe('.return', function () { + // input should be [order, firstrow, lastrow] + it('should return an array', function(done) { + Rental.return(['Jaws', 4], function(error,rentals){ + expect(rentals).toEqual(jasmine.any(Array)) + done() + }) + }) + }) +}) diff --git a/tasks/load_schema.js b/tasks/load_schema.js new file mode 100644 index 000000000..6755c455a --- /dev/null +++ b/tasks/load_schema.js @@ -0,0 +1,32 @@ +var massive = require('massive') +var connectionString = "postgres://localhost/cassettecollection_development" +var connectionString_test = "postgres://localhost/cassettecollection_test" + +var db = massive.connectSync({connectionString : connectionString}) +var db_test = massive.connectSync({connectionString : connectionString_test}) + +var count = 0 + +db.setup.schema([], function(err, res) { + if (err) { + throw(new Error(err.message)) + } + + count += 1 + console.log("yay schema1!") + checkFinish() +}) + +db_test.setup.schema([], function(err, res) { + if (err) { + throw(new Error(err.message)) + } + + count += 1 + console.log("yay schema2!") + checkFinish() +}) + +function checkFinish() { + if (count >= 2) { process.exit() } +} diff --git a/tasks/seed_data.js b/tasks/seed_data.js new file mode 100644 index 000000000..875aaaf28 --- /dev/null +++ b/tasks/seed_data.js @@ -0,0 +1,74 @@ +var massive = require('massive') +var connectionString = "postgres://localhost/cassettecollection_development" +var connectionString_test = "postgres://localhost/cassettecollection_test" + +var db = massive.connectSync({connectionString : connectionString}) +var db_test = massive.connectSync({connectionString : connectionString_test}) + +var movies_data = require("../db/seeds/movies") +var customers_data = require("../db/seeds/customers") +var rentals_data = require("../db/seeds/rentals") + +var moviesCount = 0 +var customersCount = 0 +var rentalsCount = 0 + +function seed() { + // saveSync is not asynchronous, it's blocking, ok for stuff like this that is run every once in a while + for (var movie of movies_data) { + db.movies.save(movie, function (err,res) { + if (err) { + throw new Error(err.message) + } + }) + + db_test.movies.save(movie, function (err,res) { + if (err) { + throw new Error(err.message) + } + moviesCount++ + checkFinish() + }) + } + + for (var customer of customers_data) { + db.customers.save(customer, function (err,res) { + if (err) { + throw new Error(err.message) + } + }) + + db_test.customers.save(customer, function (err,res) { + if (err) { + throw new Error(err.message) + } + customersCount++ + checkFinish() + }) + } + + for (var rental of rentals_data) { + db.rentals.save(rental, function (err,res) { + if (err) { + throw new Error(err.message) + } + }) + + db_test.rentals.save(rental, function (err,res) { + if (err) { + throw new Error(err.message) + } + rentalsCount++ + checkFinish() + }) + } +} + +function checkFinish() { + var totalCount = moviesCount + customersCount + rentalsCount + var totalLength = movies_data.length + customers_data.length + rentals_data.length + + if (totalCount >= totalLength) { process.exit() } +} + +seed() diff --git a/views/docs.ejs b/views/docs.ejs new file mode 100644 index 000000000..8b46d6396 --- /dev/null +++ b/views/docs.ejs @@ -0,0 +1,15 @@ + + + <%= title %> + + + + + + + + <% for (var i in docs) { + <%= i %> + <% } %> + + diff --git a/views/jadedocs.jade b/views/jadedocs.jade new file mode 100644 index 000000000..0b4d8ae9e --- /dev/null +++ b/views/jadedocs.jade @@ -0,0 +1,51 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Gloria+Hallelujah|Open+Sans:300') + body + .docs-container + .docs-header + h1 Cassette Collection + img(src='http://londoninspiration.com/wp-content/uploads/2014/01/vhs.jpg') + .docs-info + p The Tunes & Takeout API allows programmatic access to randomized suggestions of specific food and music pairings, that data for which is obtained from the Yelp and Spotify APIs. + + p The API endpoint is available at https://cassettecollection.herokuapp.com/ and all paths listed in this documentation should be assumed to use that base URL. + + h2 API Specification + + p This is version 1 of the Cassette Collection. To ensure consistency if the API evolves further, all requests to version 1 are prefixed with the path /v1/. + + h2 Search for suggestions + + p Search for food and music suggestions given a particular search term, and an optional limit for number of results. + + h3 Request + + p Search requests are GET requests to the /v1/suggestions/search endpoint with the following parameters: + + p blah + + h3 Response + + p Search results are JSON documents containing a list of suggestion hashes, and a canonical URL for the request itself. Each suggestion hash includes the ID for a specific business from the Yelp API as well as an ID and type for an item from the Spotify API. + + p Both IDs are string values, and the type for the music item is one of artist, album, track. + + p Wherever possible, the Tunes & Takeout API is written to provide consistent results for the same queries. This can aid with caching, but it cannot be guaranteed. Because the Tunes & Takeout API relies upon the Yelp and Spotify APIs, when their data changes the results of particular search queries may also change. + + p If the request was not successful, the following HTTP status codes may be returned depending on the specific error: + + p 400 - The request parameters were invalid. Either the query was missing, or the limit was not an integer between 1 and 100. + + h2 Examples + + h3 Simple query without a limit + + p Request URL: + + p GET: + + code /v1/suggestions/search?query=banana diff --git a/views/layout.jade b/views/layout.jade index 15af079bf..af974804d 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -3,5 +3,6 @@ html head title= title link(rel='stylesheet', href='/stylesheets/style.css') + link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Gloria+Hallelujah|Open+Sans:300') body block content