Skip to content

Commit

Permalink
feat(article): implement like and dislike functionality for articles
Browse files Browse the repository at this point in the history
- create like or dislike article endpoint
- create article like controller

[Finishes #159987408]
  • Loading branch information
tomiadebanjo committed Sep 13, 2018
1 parent cb75b04 commit 7631498
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 43 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ package-lock.json

# environmental variables
.env

# bash file
export_env.bash
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"description": "A Social platform for the creative at heart",
"main": "app.js",
"scripts": {
"pretest": "NODE_ENV=test sequelize db:migrate;NODE_ENV=test sequelize db:seed:all",
"test": "NODE_ENV=test nyc mocha --exit --require babel-core/register",
"pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all",
"test": "NODE_ENV=test nyc mocha test/**.spec.js --require babel-polyfill --require babel-core/register --exit",
"posttest": "NODE_ENV=test sequelize db:migrate:undo:all",
"test:dev": "npm run posttest && npm test",
"start": "babel-node server/app.js",
Expand Down
92 changes: 59 additions & 33 deletions server/controllers/articleController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import cloudinaryConfig from '../config/cloudinary/cloudinaryConfig';
import ratingHelpers from '../helpers/ratingHelpers';
import models from '../models';

const { Articles, Ratings } = models;
const { Articles, Ratings, ArticleLikes } = models;
const { analyseRatings } = ratingHelpers;

/**
Expand All @@ -19,14 +19,16 @@ const articlesController = {
* @param {object} res http response object
* @returns {res} http response object
*/
create: (req, res) => {
create: async (req, res) => {
const { files, fields } = req.articleFormData;
let imageUrl = null;
// check for image
if (files.image) {
const temporaryPath = files.image.path;
// INITIALIZES CLOUDINARY LOCAL CONFIGURATIONS
cloudinaryConfig();
// SAVES IMAGE TO CLOUDINARY
cloudinary.v2.uploader.upload(
imageUrl = await cloudinary.v2.uploader.upload(
temporaryPath, (error, result) => {
if (error) {
return res.status(500).jsend.fail({
Expand All @@ -35,40 +37,64 @@ const articlesController = {
formData: fields,
});
}
// VALIDATE
// SAVE ARTICLE WITH IMAGE
Articles.create({
userId: req.currentUser.id,
title: fields.title,
slug: `${slug(fields.title)}-${uuid()}`,
description: fields.description,
body: fields.body,
imageUrl: result.url,
}).then(createdArticle => res.status(201).jsend.success({
article: createdArticle,
message: 'Article published successfully'
})).catch(err => res.status(500).jsend.fail({
message: 'Something, went wrong. please try again',
return result;
}
);
imageUrl = imageUrl.url;
}
// SAVE ARTICLE
Articles.create({
userId: req.currentUser.id,
title: fields.title,
slug: `${slug(fields.title)}-${uuid()}`,
description: fields.description,
body: fields.body,
imageUrl
}).then(createdArticle => res.status(201).jsend.success({
article: createdArticle,
message: 'Article published successfully'
})).catch(err => res.status(500).jsend.fail({
message: 'Something, went wrong. please try again',
error: err.message
}));
},
/**
* @description this function likes, dislikes and unlike an article
* @param {Object} req the request object
* @param {Object} res the response object
* @returns {Object} json response
*/
like: (req, res) => {
// Check the params passed by user to determine what function to be performed
const like = req.params.likeType === 'like';
const dislike = req.params.likeType === 'dislike';
// message to be sent to user depending on function performed
const message = like || dislike ? `you ${req.params.likeType}d the article`
: 'you unliked the article';
ArticleLikes
.findOrCreate({
where: {
userId: req.currentUser.id,
articleId: Number(req.params.articleId)
},
defaults: {
like,
dislike
}
})
.spread((data, created) => {
if (!created) {
data.like = like;
data.dislike = dislike;
data.save().catch(err => res.status(500).jsend.error({
message: 'Request could not be processed',
error: err.message
}));
}
);
} else {
// SAVE ARTICLE WITHOUT IMAGE
Articles.create({
userId: req.currentUser.id,
title: fields.title,
slug: `${slug(fields.title)}-${uuid()}`,
description: fields.description,
body: fields.body
}).then(createdArticle => res.status(201).jsend.success({
article: createdArticle,
message: 'Article published successfully'
})).catch(err => res.status(500).jsend.fail({
message: 'Something, went wrong. please try again',
error: err.message
return res.status(200).jsend.success({ data, message });
}).catch(() => res.status(401).jsend.fail({
message: 'Article not found'
}));
}
},

/**
Expand Down
38 changes: 38 additions & 0 deletions server/middleware/checkParams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const checkParams = {
/**
* @description This method validates the params passed in by user for likeType
* @param {object} req The request object
* @param {object} res The response object
* @param {function} next Calls the next middleware
* @returns {object} undefined
*/
likeType: (req, res, next) => {
const validParams = ['like', 'unlike', 'dislike'];
if (!validParams.includes(req.params.likeType.toLowerCase())) {
return res.status(401).jsend.fail({
message: 'Invalid likeType... likeType has to be - like, dislike or unlike'
});
}
next();
},
/**
* @description This method validates the params passed in by user for id
* @param {object} req The request object
* @param {object} res The response object
* @param {function} next Calls the next middleware
* @returns {object} undefined
*/
id: (req, res, next) => {
const { articleId } = req.params;
const numberRegex = /^[0-9]+$/;
// check articleId if it is passed
if (articleId) {
if (!numberRegex.test(articleId)) {
return res.status(400).jsend.fail({ message: 'Invalid articleId' });
}
}
return next();
}
};

export default checkParams;
45 changes: 45 additions & 0 deletions server/migrations/20180912053024-create-article-likes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@


module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('ArticleLikes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references: {
model: 'Users',
key: 'id',
as: 'userId'
}
},
articleId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references: {
model: 'Articles',
key: 'id',
as: 'articleId'
}
},
like: {
type: Sequelize.BOOLEAN
},
dislike: {
type: Sequelize.BOOLEAN
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('ArticleLikes')
};
34 changes: 34 additions & 0 deletions server/models/articlelikes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const articleLikes = (sequelize, DataTypes) => {
const ArticleLikes = sequelize.define('ArticleLikes', {
userId: {
type: DataTypes.INTEGER,
allowNull: false
},
articleId: {
type: DataTypes.INTEGER,
allowNull: false
},
like: {
type: DataTypes.BOOLEAN,
allowNull: false
},
dislike: {
type: DataTypes.BOOLEAN,
allowNull: false
}
}, {});
ArticleLikes.associate = (models) => {
ArticleLikes.belongsTo(models.Users, {
foreignKey: 'userId',
onDelete: 'CASCADE'
});

ArticleLikes.belongsTo(models.Articles, {
foreignKey: 'articleId',
onDelete: 'CASCADE'
});
};
return ArticleLikes;
};

export default articleLikes;
5 changes: 5 additions & 0 deletions server/models/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const articles = (sequelize, DataTypes) => {
as: 'favourites'
});

Articles.hasMany(models.ArticleLikes, {
foreignKey: 'articleId',
as: 'articleLikes'
});

Articles.belongsToMany(models.Tags, {
as: 'articleTags',
through: 'Tagging',
Expand Down
4 changes: 4 additions & 0 deletions server/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const users = (sequelize, DataTypes) => {
foreignKey: 'userId',
as: 'ratings'
});
Users.hasMany(models.ArticleLikes, {
foreignKey: 'userId',
as: 'articleLikes'
});
};
Users.beforeCreate((user) => {
user.password = user.password ? bcrypt.hashSync(user.password, 8) : null;
Expand Down
12 changes: 5 additions & 7 deletions server/routes/articleRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ import auth from '../middleware/auth';
import articleController from '../controllers/articleController';
import ratingValidation from '../middleware/ratingValidation';
import inputValidator from '../middleware/inputValidator';
import checkParams from '../middleware/checkParams';

const {
rateArticle,
create
} = articleController;
const { rateArticle, create } = articleController;
const {
validArticleId,
validateRating,
validateObject,
validateUser
} = ratingValidation;
const {
validateArticle
} = inputValidator;
const { validateArticle } = inputValidator;

const articleRoutes = express.Router();

Expand All @@ -31,5 +27,7 @@ articleRoutes.post(
validateUser,
rateArticle
);
articleRoutes.post('/', auth, inputValidator.validateArticle, articleController.create);
articleRoutes.post('/:articleId/:likeType', auth, checkParams.id, checkParams.likeType, articleController.like);

export default articleRoutes;
46 changes: 45 additions & 1 deletion swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
{
"name": "Articles",
"description": "Everything about all Articles"
},
{
"name": "Like",
"description": "Everything about all likes"
}
],
"schemes": [
Expand Down Expand Up @@ -155,7 +159,47 @@
"description": "Failed to authenticate token! Valid token required"
}
}
}
},
"/api/v1/articles/{articleId}/{likeType}": {
"put": {
"tags": [ "Like" ],
"summary": "Allows user to like, dislike and unlike article",
"description": "Article like, dislike and unlike endpoint",
"consumes": [
"application/json"
],
"produces": [
"application/xml"
],
"parameters": [
{
"in": "path",
"name": "articleId",
"description": "The id of the article to be liked",
"required": true,
"type": "number"
},
{
"in": "path",
"name": "likeType",
"description": "The likeType the user inputs.. The acceptable liketypes are like, dislike and unlike",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "Password reset successful"
},
"401": {
"description": "Verification link not valid"
},
"500": {
"description": "Request could not be completed. Please try again"
}
}
}
}
},
"/api/v1/users/{userId}/follow": {
"post": {
Expand Down
Loading

0 comments on commit 7631498

Please sign in to comment.