diff --git a/controllers/article.js b/controllers/article.js index a355591..58cbbbf 100644 --- a/controllers/article.js +++ b/controllers/article.js @@ -1,7 +1,9 @@ +/* eslint-disable guard-for-in */ import models from '../models/index'; import readingTime from '../helpers/readingTime'; const Article = models.article; +const Votes = models.vote; /** * @param {class} --Article controller */ @@ -83,6 +85,7 @@ class ArticleController { * @returns {Object} return article */ static singleArticle(req, res) { + const user = (req.user ? req.user.id : 'nouser'); Article.findByPk(req.params.articleId) .then((article) => { if (!article) { @@ -90,8 +93,32 @@ class ArticleController { } article.dataValues.readingTime = readingTime(article.title + article.body); // @return article - return res.status(200).json({ status: 200, article }); - }); + Votes.findAll({ where: { article: req.params.articleId } }) + .then((vote) => { + let countLike = 0; + let countDisLike = 0; + let hasLiked = false; + let hasDisliked = false; + // eslint-disable-next-line no-restricted-syntax + for (const i in vote) { + countLike += vote[i].like; + countDisLike += vote[i].dislike; + if (vote[i].user === user) { + hasLiked = vote[i].like; + hasDisliked = vote[i].dislike; + } + } + // @return article + + return res.status(200).json({ + status: 200, article, likes: countLike, dislike: countDisLike, hasLiked, hasDisliked + }); + }) + .catch((error) => { + console.log(error); + }); + }) + .catch(error => res.status(500).json({ error: `Something wrong please try again later. ${error}` })); } } diff --git a/controllers/votes.js b/controllers/votes.js new file mode 100644 index 0000000..9861837 --- /dev/null +++ b/controllers/votes.js @@ -0,0 +1,75 @@ +import models from '../models/index'; + +const Votes = models.vote; + +/** * + * @param {request } req request + * @param {response} res response + */ + +// eslint-disable-next-line require-jsdoc +class VotesController { + /** * + * @param {request } req request + * @param {response} res response + * @returns {message} message + */ + static async likes(req, res) { + try { + const likeData = { + user: req.user.id, + article: req.params.articleId, + like: true, + dislike: false + }; + if (req.vote === null) { + await Votes.create(likeData); + return res.status(200).json({ message: 'thanks for the support.' }); + } if (req.vote.like === true) { + return res.status(400).json({ error: 'sorry you have already liked this article.' }); + } + await Votes.update(likeData, { where: { vote_id: req.vote.vote_id } }); + return res.status(200).json({ + message: 'thanks for the support.', + userId: req.user.id, + article: req.params.articleId, + }); + } catch (error) { + return res.status(500).json({ error: 'something wrong try again later.' }); + } + } + + /** * +* @param {request } req request +* @param {response} res response +* @returns {message} message +*/ + + // eslint-disable-next-line require-jsdoc + static async dislikes(req, res) { + try { + const dislikeData = { + user: req.user.id, + article: req.params.articleId, + like: false, + dislike: true + }; + if (req.vote === null) { + await Votes.create(dislikeData); + return res.status(200).json({ message: 'thank for support.' }); + } if (req.vote.dislike === true) { + return res.status(400).json({ error: 'sorry you have already disliked this article.' }); + } + await Votes.update(dislikeData, { where: { vote_id: req.vote.vote_id } }); + return res.status(200).json({ + message: 'You have disliked this article.', + userId: req.user.id, + article: req.params.articleId, + }); + } catch (error) { + return res.status(500).json({ error: 'something wrong try again later.' }); + } + } +} + +export default VotesController; diff --git a/middlewares/isAuth.js b/middlewares/isAuth.js new file mode 100644 index 0000000..91e1f44 --- /dev/null +++ b/middlewares/isAuth.js @@ -0,0 +1,13 @@ +import passport from 'passport'; + +const isAuth = (req, res, next) => { + passport.authenticate('jwt', { session: false }, (error, user) => { + if (error) { + next(error); + } + req.user = user; + next(); + })(req, res, next); +}; + +export default isAuth; diff --git a/middlewares/votes.js b/middlewares/votes.js new file mode 100644 index 0000000..a3cb4af --- /dev/null +++ b/middlewares/votes.js @@ -0,0 +1,24 @@ +import models from '../models/index'; + +const Vote = models.vote; + +// eslint-disable-next-line import/prefer-default-export +const checkVote = async (req, res) => { + try { + const search = await Vote.findOne({ where: { user: req.user.id } }); + return search; + } catch (error) { + return res.status(500).json({ status: 500, message: `something wrong please try again.${error}` }); + } +}; +const checkLikes = async (req, res, next) => { + const vote = await checkVote(req, res); + if (!vote) { + req.vote = null; + next(); + } else { + req.vote = vote; + next(); + } +}; +export default checkLikes; diff --git a/migrations/20190409175130-Vote.js b/migrations/20190409175130-Vote.js new file mode 100644 index 0000000..91209a3 --- /dev/null +++ b/migrations/20190409175130-Vote.js @@ -0,0 +1,52 @@ + + +const voteMigration = { + up: (queryInterface, Sequelize) => queryInterface.createTable('votes', + { + vote_id: { + type: Sequelize.INTEGER, + allowNull: false, + primaryKey: true, + autoIncrement: true + }, + user: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'users', + key: 'id' + } + + }, + article: { + type: Sequelize.INTEGER, + allowNull: false, + onDelete: 'CASCADE', + references: { + model: 'articles', + key: 'article_id' + } + }, + like: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + dislike: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + default: true + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }), + down: queryInterface => queryInterface.dropTable('votes') +}; + +export default voteMigration; diff --git a/models/article.js b/models/article.js index 452a2ad..628f9a6 100644 --- a/models/article.js +++ b/models/article.js @@ -13,7 +13,7 @@ const articleModel = (Sequelize, DataTypes) => { key: 'id' } }, - image: { type: DataTypes.STRING, allowNull: true } + image: { type: DataTypes.STRING, allowNull: true }, }, {}); Article.associate = (models) => { Article.hasMany(models.comments, { diff --git a/models/votes.js b/models/votes.js new file mode 100644 index 0000000..9a13ace --- /dev/null +++ b/models/votes.js @@ -0,0 +1,18 @@ +const voteModels = (Sequelize, DataTypes) => { + const Votes = Sequelize.define('vote', { + vote_id: { + type: DataTypes.INTEGER, allowNull: false, primaryKey: true, autoIncrement: true + }, + user: { type: DataTypes.INTEGER, references: { model: 'user', key: 'id' } }, + article: { type: DataTypes.INTEGER, references: { model: 'article', key: 'article_id' } }, + like: { type: DataTypes.BOOLEAN }, + dislike: { type: DataTypes.BOOLEAN }, + }, {}); + Votes.associate = (models) => { + Votes.belongsTo(models.article, { as: 'articlefkey', foreignKey: 'article' }); + Votes.belongsTo(models.user, { as: 'userfkey', foreignKey: 'user' }); + }; + return Votes; +}; + +export default voteModels; diff --git a/routes/api/article.js b/routes/api/article.js index 195ef5a..c1416f6 100644 --- a/routes/api/article.js +++ b/routes/api/article.js @@ -3,7 +3,6 @@ import passport from 'passport'; import Article from '../../controllers/article'; import Comment from '../../controllers/comment'; import multer from '../../middlewares/multerConfiguration'; -import { checkingArticle } from '../../middlewares/article'; import validateComment from '../../helpers/validateComment'; import checkComment from '../../middlewares/checkComment'; import Rate from '../../controllers/rate'; @@ -12,8 +11,15 @@ import checkArticle from '../../middlewares/checkArticle'; import asyncHandler from '../../helpers/errors/asyncHandler'; import shareArticle from '../../helpers/shareArticles'; +import { checkingArticle, findArticleExist } from '../../middlewares/article'; +import Votes from '../../controllers/votes'; +import checkVote from '../../middlewares/votes'; +import isAuth from '../../middlewares/isAuth'; + const router = express.Router(); -const auth = passport.authenticate('jwt', { session: false }); +const auth = passport.authenticate('jwt', { + session: false +}); // @Method POST // @Desc create article router.post('/', auth, multer, Article.create); @@ -25,12 +31,14 @@ router.get('/:articleId/comments/', auth, asyncHandler(checkArticle), Comment.ge router.put('/:idArticle/comments/:commentId', auth, checkComment, Comment.updateComment); // @Mehtod delete a given comment router.delete('/:idArticle/comments/:commentId', auth, checkComment, Comment.deleteComment); +router.post('/:articleId/like', auth, findArticleExist, checkVote, Votes.likes); +router.post('/:articleId/dislike', auth, findArticleExist, checkVote, Votes.dislikes); // @Method GET // @Desc get all created article router.get('/', Article.getArticle); // @Method GET // @desc get single article -router.get('/:articleId', Article.singleArticle); +router.get('/:articleId', isAuth, Article.singleArticle); // @Method PUT // @Desc update articles router.put('/:articleId', auth, checkingArticle, multer, Article.updateArticle); diff --git a/test/6-likes.test.js b/test/6-likes.test.js new file mode 100644 index 0000000..c5e93c0 --- /dev/null +++ b/test/6-likes.test.js @@ -0,0 +1,167 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import app from '../index'; +import { + article1 +} from '../testingdata/article.json'; +import { + login1 +} from '../testingdata/user.json'; + +let APItoken; +let articleId; +chai.use(chaiHttp); +chai.should(); + +describe('votes', () => { + before(async () => { + try { + const user = await chai.request(app) + .post('/api/users/login') + .set('Content-Type', 'application/json') + .send(login1); + APItoken = `Bearer ${user.body.token}`; + const article = await chai.request(app) + .post('/api/articles') + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .send(article1); + articleId = article.body.article.article_id; + } catch (error) { + console.log(error); + } + }); + it('should allow user to like', (done) => { + chai.request(app) + .post(`/api/articles/${articleId}/like`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .send({ + like: true, + dislike: false + }) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(200); + res.body.should.have.property('message'); + done(); + }); + }); + // should return status of code 404 + it('should retun status of 404 when user is going to like article', (done) => { + chai.request(app) + .post(`/api/articles/${10000}/like`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .send({ + like: true, + dislike: false + }) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(404); + res.body.should.have.property('error'); + done(); + }); + }); + // should return 400 status + it('should allow user to dislike', (done) => { + chai.request(app) + .post(`/api/articles/${articleId}/like`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .send({ + like: true, + dislike: false + }) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(400); + res.body.should.have.property('error'); + done(); + }); + }); + it('should retun status of 500 when something goes wrong ', (done) => { + chai.request(app) + .post('/api/articles/:articleId/like') + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(500); + res.body.should.have.property('error'); + done(); + }); + }); + // status code of 500 + it('should allow user to dislike', (done) => { + chai.request(app) + .post(`/api/articles/${null}/like`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(500); + res.body.should.have.property('error'); + done(); + }); + }); + it('should allow user to dislike', (done) => { + chai.request(app) + .post(`/api/articles/${articleId}/dislike`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(200); + res.body.should.have.property('message'); + done(); + }); + }); + // shoull return status code of 400 + it('should allow user to dislike', (done) => { + chai.request(app) + .post(`/api/articles/${articleId}/dislike`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .send({ + like: false, + dislike: true + }) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(400); + res.body.should.have.property('error'); + done(); + }); + }); + // shoull return status code of 500 + it('should allow user to dislike', (done) => { + chai.request(app) + .post(`/api/articles/${null}/dislike`) + .set('Content-Type', 'application/json') + .set('Authorization', APItoken) + .end((err, res) => { + if (err) { + done(err); + } + res.should.have.status(500); + res.body.should.have.property('error'); + done(); + }); + }); +}); diff --git a/test/6-comment.js b/test/8-comment.js similarity index 100% rename from test/6-comment.js rename to test/8-comment.js diff --git a/testingdata/user.json b/testingdata/user.json index 079437f..c6e8c07 100644 --- a/testingdata/user.json +++ b/testingdata/user.json @@ -49,7 +49,7 @@ "password": "1Kig1L@20" }, "googleValidToken": { - "access_token": "ya29.GlvvBqe7M-RLlus5dSVWVvHjTRlsfHrqPfww-d9urRPLj0q18SMMdDcFcW4MeoY6tP0XwOSeKcjW2yzPGgqx7hL7-QgEiKnu40OdXHyPGFBt5CT2LnqKHju7Wko7" + "access_token": "ya29.Glv0BvA5mn66F3kiMjV2saEa7zoMHTQnrgBupdZ_OFnFCY7i3RFmzfkQ6BgJvbvGdTKoSkTGJu4M6uLIEnOh-A8Omp9Qe9Ot4FUdwlM3g7FcPY0WsNHi2ie5P6KE" }, "googleInvalidToken": { "access_token": "ya29.GlvhBpzY2hl2ShgOMrpkni8obGgwyX0mr85Oendf2kmblu3BrRNTmYK2DVQiPciVOBFkLvR57YE90qDyffgJOqgzV68zutO3-Y9QDKooAPuxPvwsbsWM36wwVPHT" @@ -78,4 +78,4 @@ "username": "EmaBush" } } -} +} \ No newline at end of file