diff --git a/models/author.js b/models/author.js index 640aea6..5d0e04d 100644 --- a/models/author.js +++ b/models/author.js @@ -1,4 +1,5 @@ var mongoose = require('mongoose'); +var moment = require('moment'); var Schema = mongoose.Schema; @@ -9,26 +10,49 @@ var AuthorSchema = new Schema({ date_of_death: { type: Date } }); -// Virtual for author's full name AuthorSchema .virtual('name') .get(function () { return this.family_name + ', ' + this,first_name; }); -// Virtual for author's lifespan AuthorSchema .virtual('lifespan') .get(function () { return (this.date_of_death.getYear() - this.date_of_birth.getYear()).toString(); }); -// Virtual for author's URL AuthorSchema .virtual('url') .get(function() { return '/catalog/author/' + this._id; }); +AuthorSchema +.virtual('url') +.get(function () { + var lifetime_string=''; + if (this.date_of_birth) { + lifetime_string = moment(this.date_of_birth).format('MMM Do, YYY') + } + lifetime_string += ' - '; + if (this.date_of_death) { + lifetime_string += moment(this.date_of_death).format('MMM Do, YYY'); + } + return lifetime_string +}); + +AuthorSchema +.virtual('date_of_birth_yyyy-mm-dd') +.get(function() { + return moment(this.date_of_birth.format('YYYY-MM-DD')); +}); + +AuthorSchema +.virtual('date_of_death_yyyy-mm-dd') +.get(function() { + return moment(this.date_of_death.format('YYYY-MM-DD')); +}); + // Exports module module.exports = mongoose.model('Author', AuthorSchema); \ No newline at end of file diff --git a/models/bookinstance.js b/models/bookinstance.js index cc857c6..3eff7c4 100644 --- a/models/bookinstance.js +++ b/models/bookinstance.js @@ -1,6 +1,7 @@ var mongoose = require('mongoose'); var Schema = mongoose.Schema; +var moment = require('moment'); var BookInstanceSchema = new Schema({ book: { type: Schema.Types.ObjectId, ref: 'Book', required: true }, @@ -16,5 +17,11 @@ BookInstanceSchema return '/catalog/bookinstance/' + this._id; }); +BookInstanceSchema +.virtual('due_back_formatted') +.get(function () { + return moment(this.due_back).format('MMMM Do, YYYY'); +}); + // Exports module module.exports = mongoose.model('BookInstance', BookInstanceSchema); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f3909c3..f353667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -750,6 +750,15 @@ } } }, + "express-validator": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-5.3.1.tgz", + "integrity": "sha512-g8xkipBF6VxHbO1+ksC7nxUU7+pWif0+OZXjZTybKJ/V0aTVhuCoHbyhIPgSYVldwQLocGExPtB2pE0DqK4jsw==", + "requires": { + "lodash": "^4.17.10", + "validator": "^10.4.0" + } + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -2044,6 +2053,11 @@ } } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "mongodb": { "version": "3.1.13", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", @@ -3346,6 +3360,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 5c41d68..2df2162 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "body-parser": "^1.18.3", "concurrently": "^4.1.0", "express": "^4.16.4", + "express-validator": "^5.3.1", + "moment": "^2.24.0", "mongoose": "^5.4.22" }, "devDependencies": { diff --git a/routes/api/author.js b/routes/api/author.js index 32cda63..d97a90d 100644 --- a/routes/api/author.js +++ b/routes/api/author.js @@ -1,41 +1,193 @@ var Author = require('../../models/author'); +var Book = require('../../models/book'); +var async = require('async'); + +const { body, validationResult } = require('exress-validator/check'); +const { sanitizeBody } = require('express-validator/filter'); // Display list of all Authors. -exports.author_list = function(req, res) { - res.send('NOT IMPLEMENTED: Author list'); +exports.author_list = function(req, res, next) { + Author.find() + .sort([['family_name', 'ascending']]) + .exec(function (err, list_authors) { + if (err) { return next(err); } + + res.render('author_list', {title: 'Author List', author_list: list_authors }); + }); }; // Display detail page for a specific Author. -exports.author_detail = function(req, res) { - res.send('NOT IMPLEMENTED: Author detail: ' + req.params.id); +exports.author_detail = function(req, res, next) { + async.parallel({ + author: function(callback) { + Author.findById(req.params.id) + .exec(callback) + }, authors_books: function(callback) { + Book.find({ 'author': req.params.id },'title summary') + .exec(callback) + }, + }, function(err, results) { + if (err) { return next(err); } // Error in API usage. + if (results.author==null) { // No results. + var err = new Error('Author not found'); + err.status = 404; + return next(err); + } + + res.render('author_detail', { title: 'Author Detail', author: results.author, author_books: results.authors_books } ); + }); }; // Display Author create form on GET. -exports.author_create_get = function(req, res) { - res.send('NOT IMPLEMENTED: Author create GET'); +exports.author_create_get = function(req, res, next) { + res.render('author_form', { title: 'Create Author' }); }; // Handle Author create on POST. -exports.author_create_post = function(req, res) { - res.send('NOT IMPLEMENTED: Author create POST'); -}; +exports.author_create_post = [ + // Validate fields. + body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.') + .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'), + body('family_name').isLength({ min: 1 }).trim().withMessage('Family name must be specified.') + .isAlphanumeric().withMessage('Family name has non-alphanumeric characters.'), + body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(), + body('date_of_death', 'Invalid date of death').optional({ checkFalsy: true }).isISO8601(), + + // Sanitize fields. + sanitizeBody('first_name').escape(), + sanitizeBody('family_name').escape(), + sanitizeBody('date_of_birth').toDate(), + sanitizeBody('date_of_death').toDate(), + + // Process request after validation and sanitization. + (req, res, next) => { + + // Extract the validation errors from a request. + const errors = validationResult(req); + + if (!errors.isEmpty()) { + // There are errors. Render form again with sanitized values/errors messages. + res.render('author_form', { title: 'Create Author', author: req.body, errors: errors.array() }); + return; + } + else { + // Data from form is valid. + + // Create an Author object with escaped and trimmed data. + var author = new Author( + { + first_name: req.body.first_name, + family_name: req.body.family_name, + date_of_birth: req.body.date_of_birth, + date_of_death: req.body.date_of_death + }); + author.save(function (err) { + if (err) { return next(err); } + // Successful - redirect to new author record. + res.redirect(author.url); + }); + } + } +]; // Display Author delete form on GET. -exports.author_delete_get = function(req, res) { - res.send('NOT IMPLEMENTED: Author delete GET'); +exports.author_delete_get = function(req, res, next) { + async.parallel({ + author: function(callback) { + Author.findById(req.params.id).exec(callback) + }, authors_books: function(callback) { + Book.find({ 'author': req.params.id }).exec(callback) + }, + }, function(err, results) { + if(err) {return next(err); } + if(results.author == null) { + res.redirect('/catalog/authors'); + } + // Successful, so render + res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.author_books }); + }); }; // Handle Author delete on POST. -exports.author_delete_post = function(req, res) { - res.send('NOT IMPLEMENTED: Author delete POST'); +exports.author_delete_post = function(req, res, next) { + async.parallel({ + author: function(callback) { + Author.findById(req.body.authorid).exec(callback); + }, authors_books: function(callback) { + Book.find({ 'author': req.body.authorid }).exec(callback) + }, + }, function(err, results) { + if(err) { return next(err); } + // Success + if(results.author_books.length > 0) { + // Author has books. Render in same way as for GET route. + res.render('author_delete', { title: 'Delete Author', author: results.author, author_books: results.authors_books }); + return; + } else { + // Author has no books. Dlete object and redirect to the list of authors. + Author.findByIdAndRemove(req.body.authorid, function deleteAuthor(err) { + if(err) { return next(err); } + // Success - go to author list + res.redirect('/catalog/authors') + }) + } + }); }; // Display Author update form on GET. -exports.author_update_get = function(req, res) { - res.send('NOT IMPLEMENTED: Author update GET'); +exports.author_update_get = function (req, res, next) { + Author.findById(req.params.id, function (err, author) { + if (err) { return next(err); } + if (author == null) { // No results. + var err = new Error('Author not found'); + err.status = 404; + return next(err); + } + // Success. + res.render('author_form', { title: 'Update Author', author: author }); + }); }; // Handle Author update on POST. -exports.author_update_post = function(req, res) { - res.send('NOT IMPLEMENTED: Author update POST'); -}; \ No newline at end of file +exports.author_update_post = [ + // Validate fields. + body('first_name').isLength({ min: 1 }).trim().withMessage('First name must be specified.') + .isAlphanumeric().withMessage('First name has non-alphanumeric characters.'), + body('family_name').isLength({ min: 1 }).trim().withMessage('Family name must be specified.') + .isAlphanumeric().withMessage('Family name has non-alphanumeric characters.'), + body('date_of_birth', 'Invalid date of birth').optional({ checkFalsy: true }).isISO8601(), + body('date_of_death', 'Invalid date of death').optional({ checkFalsy: true }).isISO8601(), + + // Sanitize fields. + sanitizeBody('first_name').trim().escape(), + sanitizeBody('family_name').trim().escape(), + sanitizeBody('date_of_birth').toDate(), + sanitizeBody('date_of_death').toDate(), + + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + // Create Author object with escaped and trimmed data (and the old id!) + var author = new Author({ + first_name: req.body.first_name, + family_name: req.body.family_name, + date_of_birth: req.body.date_of_birth, + date_of_death: req.body.date_of_death, + _id: req.params.id + } + ); + if (!errors.isEmpty()) { + // There are errors. Render the form again with sanitized values and error messages. + res.render('author_form', { title: 'Update Author', author: author, errors: errors.array() }); + return; + } else { + // Data from form is valid. Update the record. + Author.findByIdAndUpdate(req.params.id, author, {}, function (err, theauthor) { + if (err) { return next(err); } + // Successful - redirect to genre detail page. + res.redirect(theauthor.url); + }); + } + } +]; \ No newline at end of file diff --git a/routes/api/book.js b/routes/api/book.js index 3e691da..2b08ec4 100644 --- a/routes/api/book.js +++ b/routes/api/book.js @@ -1,45 +1,286 @@ var Book = require('../../models/book'); +var Author = require('../../models/author'); +var Genre = require('../../models/genre'); +var BookInstance = require('../../models/bookinstance'); +var async = require('async'); + +const { body, validationResult } = require('exress-validator/check'); +const { sanitizeBody } = require('express-validator/filter'); exports.index = function(req, res) { - res.send('NOT IMPLEMENTED: Site Home Page'); + async.parallel({ + book_count: function(callback) { + Book.countDocuments({}, callback); + }, book_instance_count: function(callback) { + BookInstance.countDocuments({}, callback); + }, book_instance_available_count: function(callback) { + Book.countDocuments({status: 'Available'}, callback); + }, author_count: function(callback) { + Author.countDocuments({}, callback); + }, genre_count: function(callback) { + Genre.countDocuments({}, callback); + } + }, function(err, results) { + res.render('index', { title: 'Local Library Home', error: err, data: results }); + }); }; // Display list of all books. -exports.book_list = function(req, res) { - res.send('NOT IMPLEMENTED: Book list'); +exports.book_list = function(req, res, next) { + Book.find({}, 'title author') + .populate('author') + .exec(function (err, list_books) { + if (err) { return next(err); } + + res.render('book_list', { title: 'Book List', book_list: list_books }); + }); }; // Display detail page for a specific book. -exports.book_detail = function(req, res) { - res.send('NOT IMPLEMENTED: Book detail: ' + req.params.id); +exports.book_detail = function(req, res, next) { + async.parallel({ + book: function(callback) { + Book.findById(req.params.id) + .populate('author') + .populate('genre') + .exec(callback); + }, book_instance: function(callback) { + BookInstance.find({ 'book': req.params.id }) + .exec(callback); + }, function (err, results) { + if (err) {return next(err); } + if (results.book==null) { + var err = new Error('Book not found'); + err.status = 404; + return next(err); + } + + res.render('book_detail', { title: 'Title', book: results.book, book_instances: results.book_instance }); + } + }); }; // Display book create form on GET. -exports.book_create_get = function(req, res) { - res.send('NOT IMPLEMENTED: Book create GET'); +exports.book_create_get = function(req, res, next) { + // Get all authors and genres, which we can use for adding to our book. + async.parallel({ + authors: function(callback){ + Author.find(callback); + }, genres: function(callback) { + Genre.find(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + res.render('book_form', { title: 'Crete Book', authors: results.authors, genres: results.genres }); + }); }; // Handle book create on POST. -exports.book_create_post = function(req, res) { - res.send('NOT IMPLEMENTED: Book create POST'); -}; +exports.book_create_post = [ + // Convert the genre to an array. + (req, res, next) => { + if(!(req.body.genre instanceof Array)){ + if(typeof req.body.genre==='undefined') + req.body.genre=[]; + else + req.body.genre=new Array(req.body.genre); + } next(); + }, + + // Validate fields. + body('title', 'Title must not be empty.').isLength({ min: 1 }).trim(), + body('author', 'Author must not be empty.').isLength({ min: 1 }).trim(), + body('summary', 'Summary must not be empty.').isLength({ min: 1 }).trim(), + body('isbn', 'ISBN must not be empty').isLength({ min: 1 }).trim(), + // Sanitize fields (using wildcard). + sanitizeBody('*').escape(), + + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + // Create a Book object with escaped and trimmed data. + var book = new Book({ + title: req.body.title, + author: req.body.author, + summary: req.body.summary, + isbn: req.body.isbn, + genre: req.body.genre + }); + + if (!errors.isEmpty()) { + // There are errors. Render form again with sanitized values/error messages. + // Get all authors and genres for form. + async.parallel({ + authors: function(callback) { + Author.find(callback); + }, genres: function(callback) { + Genre.find(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + // Mark our selected genres as checked. + for (let i = 0; i < results.genres.length; i++) { + if (book.genre.indexOf(results.genres[i]._id) > -1) { + results.genres[i].checked='true'; + } + } + res.render('book_form', { title: 'Create Book',authors:results.authors, genres:results.genres, book: book, errors: errors.array() }); + }); + return; + } else { + // Data from form is valid. Save book. + book.save(function (err) { + if (err) { return next(err); } + //successful - redirect to new book record. + res.redirect(book.url); + }); + } + } +]; // Display book delete form on GET. -exports.book_delete_get = function(req, res) { - res.send('NOT IMPLEMENTED: Book delete GET'); +exports.book_delete_get = function(req, res, next) { + async.parallel({ + book: function(callback) { + Book.findById(req.params.id).populate('author').populate('genre').exec(callback); + }, book_bookinstances: function(callback) { + BookInstance.find({ 'book': req.params.id }).exec(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + if (results.book==null) { // No results. + res.redirect('/catalog/books'); + } + // Successful, so render. + res.render('book_delete', { title: 'Delete Book', book: results.book, book_instances: results.book_bookinstances } ); + }); }; // Handle book delete on POST. -exports.book_delete_post = function(req, res) { - res.send('NOT IMPLEMENTED: Book delete POST'); +exports.book_delete_post = function(req, res, next) { + // Assume the post has valid id (ie no validation/sanitization). + async.parallel({ + book: function(callback) { + Book.findById(req.params.id).populate('author').populate('genre').exec(callback); + }, book_bookinstances: function(callback) { + BookInstance.find({ 'book': req.params.id }).exec(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + // Success + if (results.book_bookinstances.length > 0) { + // Book has book_instances. Render in same way as for GET route. + res.render('book_delete', { title: 'Delete Book', book: results.book, book_instances: results.book_bookinstances } ); + return; + } else { + // Book has no BookInstance objects. Delete object and redirect to the list of books. + Book.findByIdAndRemove(req.body.id, function deleteBook(err) { + if (err) { return next(err); } + // Success - got to books list. + res.redirect('/catalog/books'); + }); + } + }); }; // Display book update form on GET. -exports.book_update_get = function(req, res) { - res.send('NOT IMPLEMENTED: Book update GET'); +exports.book_update_get = function(req, res, next) { + // Get book, authors and genres for form. + async.parallel({ + book: function(callback) { + Book.findById(req.params.id).populate('author').populate('genre').exec(callback); + }, authors: function(callback) { + Author.find(callback); + }, genres: function(callback) { + Genre.find(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + if (results.book==null) { // No results. + var err = new Error('Book not found'); + err.status = 404; + return next(err); + } + // Success. + // Mark our selected genres as checked. + for (var all_g_iter = 0; all_g_iter < results.genres.length; all_g_iter++) { + for (var book_g_iter = 0; book_g_iter < results.book.genre.length; book_g_iter++) { + if (results.genres[all_g_iter]._id.toString()==results.book.genre[book_g_iter]._id.toString()) { + results.genres[all_g_iter].checked='true'; + } + } + } + res.render('book_form', { title: 'Update Book', authors:results.authors, genres:results.genres, book: results.book }); + }); }; // Handle book update on POST. -exports.book_update_post = function(req, res) { - res.send('NOT IMPLEMENTED: Book update POST'); -}; \ No newline at end of file +exports.book_update_post = [ + // Convert the genre to an array + (req, res, next) => { + if(!(req.body.genre instanceof Array)){ + if(typeof req.body.genre==='undefined') + req.body.genre=[]; + else + req.body.genre=new Array(req.body.genre); + } next(); + }, + + // Validate fields. + body('title', 'Title must not be empty.').isLength({ min: 1 }).trim(), + body('author', 'Author must not be empty.').isLength({ min: 1 }).trim(), + body('summary', 'Summary must not be empty.').isLength({ min: 1 }).trim(), + body('isbn', 'ISBN must not be empty').isLength({ min: 1 }).trim(), + + // Sanitize fields. + sanitizeBody('title').escape(), + sanitizeBody('author').escape(), + sanitizeBody('summary').escape(), + sanitizeBody('isbn').escape(), + sanitizeBody('genre.*').escape(), + + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + // Create a Book object with escaped/trimmed data and old id. + var book = new Book({ + title: req.body.title, + author: req.body.author, + summary: req.body.summary, + isbn: req.body.isbn, + genre: (typeof req.body.genre==='undefined') ? [] : req.body.genre, + _id:req.params.id //This is required, or a new ID will be assigned! + }); + + if (!errors.isEmpty()) { + // There are errors. Render form again with sanitized values/error messages. + // Get all authors and genres for form. + async.parallel({ + authors: function(callback) { + Author.find(callback); + }, genres: function(callback) { + Genre.find(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + // Mark our selected genres as checked. + for (let i = 0; i < results.genres.length; i++) { + if (book.genre.indexOf(results.genres[i]._id) > -1) { + results.genres[i].checked='true'; + } + } + res.render('book_form', { title: 'Update Book',authors:results.authors, genres:results.genres, book: book, errors: errors.array() }); + }); + return; + } else { + // Data from form is valid. Update the record. + Book.findByIdAndUpdate(req.params.id, book, {}, function (err,thebook) { + if (err) { return next(err); } + // Successful - redirect to book detail page. + res.redirect(thebook.url); + }); + } + } +]; \ No newline at end of file diff --git a/routes/api/bookinstance.js b/routes/api/bookinstance.js index d98b1cc..ce391ad 100644 --- a/routes/api/bookinstance.js +++ b/routes/api/bookinstance.js @@ -1,41 +1,179 @@ var BookInstance = require('../../models/bookinstance'); +var Book = require('../../models/book'); +var async = require('async'); + +const { body,validationResult } = require('express-validator/check'); +const { sanitizeBody } = require('express-validator/filter'); // Display list of all BookInstances. -exports.bookinstance_list = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance list'); +exports.bookinstance_list = function(req, res, next) { + BookInstance.find() + .populate('book') + .exec(function (err, list_bookinstances) { + if (err) { return next(err); } + + res.render('bookinstance_list', { title: 'Book Instance List', bookinstance_list: list_bookinstances }); + }); }; // Display detail page for a specific BookInstance. -exports.bookinstance_detail = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance detail: ' + req.params.id); +exports.bookinstance_detail = function(req, res, next) { + BookInstance.findById(req.params.id) + .populate('book') + .exec(function (err, bookinstance) { + if (err) { return next(err); } + if (bookinstance==null) { // No results. + var err = new Error('Book copy not found'); + err.status = 404; + return next(err); + } + + res.render('bookinstance_detail', { title: 'Book:', bookinstance: bookinstance}); + }) }; // Display BookInstance create form on GET. -exports.bookinstance_create_get = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance create GET'); +exports.bookinstance_create_get = function(req, res, next) { + Book.find({}, 'title') + .exec(function (err, books) { + if(err) { return next(err); } + // Successful, so render. + res.render('bookinstance_form', { title: 'Create BookInstance', book_list: books }); + }) }; // Handle BookInstance create on POST. -exports.bookinstance_create_post = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance create POST'); -}; +exports.bookinstance_create_post = [ + // Validate fields. + body('book', 'Book must be specified').isLength({ min: 1 }).trim(), + body('imprint', 'Imprint must be specified').isLength({ min: 1 }).trim(), + body('due_back', 'Invalid date').optional({ checkFalsy: true }).isISO8601(), + + // Sanitize fields. + sanitizeBody('book').escape(), + sanitizeBody('imprint').escape(), + sanitizeBody('status').trim().escape(), + sanitizeBody('due_back').toDate(), + + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + // Create a BookInstance object with escaped and trimmed data. + var bookinstance = new BookInstance({ + book: req.body.book, + imprint: req.body.imprint, + status: req.body.status, + due_back: req.body.due_back + }); + + if (!errors.isEmpty()) { + // There are errors. Render form again with sanitized values and error messages. + Book.find({},'title') + .exec(function (err, books) { + if (err) { return next(err); } + // Successful, so render. + res.render('bookinstance_form', { title: 'Create BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors.array(), bookinstance:bookinstance }); + }); + return; + } else { + // Data from form is valid. + bookinstance.save(function (err) { + if (err) { return next(err); } + // Successful - redirect to new record. + res.redirect(bookinstance.url); + }); + } + } +]; // Display BookInstance delete form on GET. -exports.bookinstance_delete_get = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance delete GET'); +exports.bookinstance_delete_get = function(req, res, next) { + BookInstance.findById(req.params.id) + .populate('book') + .exec(function (err, bookinstance) { + if (err) { return next(err); } + if (bookinstance==null) { // No results. + res.redirect('/catalog/bookinstances'); + } + // Successful, so render. + res.render('bookinstance_delete', { title: 'Delete BookInstance', bookinstance: bookinstance}); + }) }; // Handle BookInstance delete on POST. -exports.bookinstance_delete_post = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance delete POST'); +exports.bookinstance_delete_post = function(req, res, next) { + // Assume valid BookInstance id in field. + BookInstance.findByIdAndRemove(req.body.id, function deleteBookInstance(err) { + if (err) { return next(err); } + // Success, so redirect to list of BookInstance items. + res.redirect('/catalog/bookinstances'); + }); }; // Display BookInstance update form on GET. -exports.bookinstance_update_get = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance update GET'); +exports.bookinstance_update_get = function(req, res, next) { + // Get book, authors and genres for form. + async.parallel({ + bookinstance: function(callback) { + BookInstance.findById(req.params.id).populate('book').exec(callback) + }, books: function(callback) { + Book.find(callback) + }, + }, function(err, results) { + if (err) { return next(err); } + if (results.bookinstance==null) { // No results. + var err = new Error('Book copy not found'); + err.status = 404; + return next(err); + } + // Success. + res.render('bookinstance_form', { title: 'Update BookInstance', book_list : results.books, selected_book : results.bookinstance.book._id, bookinstance:results.bookinstance }); + }); }; -// Handle bookinstance update on POST. -exports.bookinstance_update_post = function(req, res) { - res.send('NOT IMPLEMENTED: BookInstance update POST'); -}; \ No newline at end of file +// Handle BookInstance update on POST. +exports.bookinstance_update_post = [ + // Validate fields. + body('book', 'Book must be specified').isLength({ min: 1 }).trim(), + body('imprint', 'Imprint must be specified').isLength({ min: 1 }).trim(), + body('due_back', 'Invalid date').optional({ checkFalsy: true }).isISO8601(), + + // Sanitize fields. + sanitizeBody('book').trim().escape(), + sanitizeBody('imprint').trim().escape(), + sanitizeBody('status').trim().escape(), + sanitizeBody('due_back').toDate(), + + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + // Create a BookInstance object with escaped/trimmed data and current id. + var bookinstance = new BookInstance({ + book: req.body.book, + imprint: req.body.imprint, + status: req.body.status, + due_back: req.body.due_back, + _id: req.params.id + }); + + if (!errors.isEmpty()) { + // There are errors so render the form again, passing sanitized values and errors. + Book.find({},'title') + .exec(function (err, books) { + if (err) { return next(err); } + // Successful, so render. + res.render('bookinstance_form', { title: 'Update BookInstance', book_list : books, selected_book : bookinstance.book._id , errors: errors.array(), bookinstance:bookinstance }); + }); + return; + } else { + // Data from form is valid. + BookInstance.findByIdAndUpdate(req.params.id, bookinstance, {}, function (err,thebookinstance) { + if (err) { return next(err); } + // Successful - redirect to detail page. + res.redirect(thebookinstance.url); + }); + } + } +]; \ No newline at end of file diff --git a/routes/api/genre.js b/routes/api/genre.js index ae40c8c..ca3f5cb 100644 --- a/routes/api/genre.js +++ b/routes/api/genre.js @@ -1,41 +1,174 @@ var Genre = require('../../models/genre'); +var Book = require('../../models/book'); +var async = require('async'); + +const { body, validationResult } = require('exress-validator/check'); +const { sanitizeBody } = require('express-validator/filter'); // Display list of all Genre. -exports.genre_list = function(req, res) { - res.send('NOT IMPLEMENTED: Genre list'); +exports.genre_list = function(req, res, next) { + Genre.find() + .sort([['name', 'ascending']]) + .exec(function (err, list_genres) { + if (err) { return next(err); } + + res.render('genre_list', {title: 'Genre List', genre_list: list_genres }); + }); }; // Display detail page for a specific Genre. -exports.genre_detail = function(req, res) { - res.send('NOT IMPLEMENTED: Genre detail: ' + req.params.id); +exports.genre_detail = function(req, res, next) { + async.parallel({ + genre: function(callback) { + Genre.findById(req.params.id) + .exec(callback); + }, genre_books: function(callback) { + Book.find({ 'genre': req.params.id }) + .exec(callback); + }, function (err, results) { + if (err) { return next(err); } + if (results.genre==null) { + var err = new Error('Genre not found'); + err.status = 404; + return next(err); + } + res.render('genre_detail', { title: 'Genre Detail', genre: results.genre, genre_books: results.genre_books }); + } + }); }; // Display Genre create form on GET. -exports.genre_create_get = function(req, res) { - res.send('NOT IMPLEMENTED: Genre create GET'); +exports.genre_create_get = function(req, res, next) { + res.render('genre_form', { title: 'Create Genre' }); }; // Handle Genre create on POST. -exports.genre_create_post = function(req, res) { - res.send('NOT IMPLEMENTED: Genre create POST'); -}; +exports.genre_create_post = [ + // Validate that the name field is not empty. + body('name', 'Genre name required').isLength({ min: 1 }).trim(), + // Sanitize (escape) the name field. + sanitizeBody('name').escape(), + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request. + const errors = validationResult(req); + + // Create a genre object with escaped and trimmed data. + var genre = new Genre( { name: req.body.name } ); + + if (!errors.isEmpty()) { + // There are errors. Render the form again with sanitized values/error messages. + res.render('genre_form', { title: 'Create Genre', genre: genre, errors: errors.array()}); + return; + } else { + // Data from form is valid. + // Check if Genre with same name already exists. + Genre.findOne({ 'name': req.body.name }) + .exec( function(err, found_genre) { + if (err) { return next(err); } + + if (found_genre) { + // Genre exists, redirect to its detail page. + res.redirect(found_genre.url); + } else { + genre.save(function (err) { + if (err) { return next(err); } + // Genre saved. Redirect to genre detail page. + res.redirect(genre.url); + }); + } + }); + } + } + ]; // Display Genre delete form on GET. -exports.genre_delete_get = function(req, res) { - res.send('NOT IMPLEMENTED: Genre delete GET'); +exports.genre_delete_get = function(req, res, next) { + async.parallel({ + genre: function(callback) { + Genre.findById(req.params.id).exec(callback); + }, genre_books: function(callback) { + Book.find({ 'genre': req.params.id }).exec(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + if (results.genre==null) { // No results. + res.redirect('/catalog/genres'); + } + // Successful, so render. + res.render('genre_delete', { title: 'Delete Genre', genre: results.genre, genre_books: results.genre_books } ); + }); }; // Handle Genre delete on POST. -exports.genre_delete_post = function(req, res) { - res.send('NOT IMPLEMENTED: Genre delete POST'); +exports.genre_delete_post = function(req, res, next) { + async.parallel({ + genre: function(callback) { + Genre.findById(req.params.id).exec(callback); + }, genre_books: function(callback) { + Book.find({ 'genre': req.params.id }).exec(callback); + }, + }, function(err, results) { + if (err) { return next(err); } + // Success + if (results.genre_books.length > 0) { + // Genre has books. Render in same way as for GET route. + res.render('genre_delete', { title: 'Delete Genre', genre: results.genre, genre_books: results.genre_books } ); + return; + } else { + // Genre has no books. Delete object and redirect to the list of genres. + Genre.findByIdAndRemove(req.body.id, function deleteGenre(err) { + if (err) { return next(err); } + // Success - go to genres list. + res.redirect('/catalog/genres'); + }); + } + }); }; // Display Genre update form on GET. -exports.genre_update_get = function(req, res) { - res.send('NOT IMPLEMENTED: Genre update GET'); +exports.genre_update_get = function(req, res, next) { + Genre.findById(req.params.id, function(err, genre) { + if (err) { return next(err); } + if (genre==null) { // No results. + var err = new Error('Genre not found'); + err.status = 404; + return next(err); + } + // Success. + res.render('genre_form', { title: 'Update Genre', genre: genre }); + }); }; // Handle Genre update on POST. -exports.genre_update_post = function(req, res) { - res.send('NOT IMPLEMENTED: Genre update POST'); -}; \ No newline at end of file +exports.genre_update_post = [ + // Validate that the name field is not empty. + body('name', 'Genre name required').isLength({ min: 1 }).trim(), + + // Sanitize (trim and escape) the name field. + sanitizeBody('name').trim().escape(), + + // Process request after validation and sanitization. + (req, res, next) => { + // Extract the validation errors from a request . + const errors = validationResult(req); + // Create a genre object with escaped and trimmed data (and the old id!) + var genre = new Genre({ + name: req.body.name, + _id: req.params.id + }); + + if (!errors.isEmpty()) { + // There are errors. Render the form again with sanitized values and error messages. + res.render('genre_form', { title: 'Update Genre', genre: genre, errors: errors.array()}); + return; + } else { + // Data from form is valid. Update the record. + Genre.findByIdAndUpdate(req.params.id, genre, {}, function (err,thegenre) { + if (err) { return next(err); } + // Successful - redirect to genre detail page. + res.redirect(thegenre.url); + }); + } + } +]; \ No newline at end of file