Skip to content

Commit

Permalink
Merge 1a30340 into 81b919d
Browse files Browse the repository at this point in the history
  • Loading branch information
Dom58 committed Jul 16, 2019
2 parents 81b919d + 1a30340 commit d680356
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 2 deletions.
1 change: 1 addition & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ TWITTER_APP_SECRET="4e6IHRuSdKfyMeEgQNAfsla3oQNXumsBKCwEMcJcsjPtRfBJhA"
TWITTER_ACCESS_TOKEN="1063823892-tZLv5zh2kwpWZoEPRZrrXRVD3EvMUsO0VqB7DVS"
TWITTER_ACCESS_SECRET="cCJmqIE7Gf2xuttD37A4uzdn1GRGuDTVjrb2goPyJre4j"
CALLBACK="http://127.0.0.1:7000/auth/twitter/callback"
DEFAULT_PASSWORD="3gh45j6k7l65k4jh3"

CLOUDINARY_CLOUD_NAME="username"
CLOUDINARY_API_KEY="api-key"
Expand Down
63 changes: 63 additions & 0 deletions src/controllers/articleRatingControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import model from '../db/models/index';

const { Users, Articles, Rating } = model;

/**
* rating controller
*/
class ArticleManager {
/**
*
* @param {Object} req
* @param {Object} res
* @returns {Object} return object
*/
static async rateArticle(req, res) {
const { slug } = req.params;
const { rating } = req.body;

const findArticle = await Articles.findOne({ where: { slug } });
if (!findArticle) {
return res.status(404).send({
error: 'Article not found!'
});
}
const findUser = await Users.findOne({ where: { email: req.user.email } });

if (findUser.id === findArticle.postedBy) {
return res.status(400).json({
error: 'You can not rate your article!'
});
}

const findIfUserRatedArticle = await Rating.findOne({
where: {
user: findUser.id,
articleId: findArticle.id
}
});
if (findIfUserRatedArticle) {
return res.status(400).json({
error: 'You have already rated this article'
});
}

const articleRatings = await Rating.findOne({ where: { articleId: findArticle.id, rating } });
if (articleRatings == null) {
const saveRating = await Rating.create({
rating,
user: findUser.id,
articleId: findArticle.id
});
return res.status(201).json({
message: 'Article rated successfuly',
data: {
user: saveRating.user,
rating: saveRating.rating
},
});
}
return res.status(400).json({ error: 'You cannot rate article twice' });
}
}
export default ArticleManager;
49 changes: 49 additions & 0 deletions src/db/migrations/20190710174425-create-rating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Ratings', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID
},
rating: {
type: Sequelize.INTEGER,
allowNull: false
},
user: {
type: Sequelize.INTEGER,
allowNull: false,
onDelete: 'CASCADE',
references: {
model: 'Users',
key: 'id',
as: 'user'
}
},
articleId: {
type: Sequelize.INTEGER,
allowNull: false,
onDelete: 'CASCADE',
references: {
model: 'Articles',
key: 'id',
as: 'articleId'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Ratings');
}
};
4 changes: 3 additions & 1 deletion src/db/models/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ export default (sequelize, DataTypes) => {
slug: DataTypes.STRING,
}, {});
Articles.associate = function(models) {
// associations can be defined here
Articles.belongsTo(models.Users, {
foreignKey: 'postedBy',
onDelete: 'CASCADE'
});
Articles.hasMany(models.Rating, {
foreignKey: 'articleId'
});
};
return Articles;
}
2 changes: 1 addition & 1 deletion src/db/models/blacklisttokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ module.exports = (sequelize, DataTypes) => {
// associations can be defined here
};
return BlacklistTokens;
};
};
34 changes: 34 additions & 0 deletions src/db/models/rating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';
module.exports = (sequelize, DataTypes) => {
const Rating = sequelize.define('Rating', {
id: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4
},
rating: {
type: DataTypes.INTEGER,
allowNull: false
},
user: {
type: DataTypes.INTEGER,
allowNull: false
},
articleId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {});
Rating.associate = (models) => {
Rating.belongsTo(models.Users, {
foreignKey: 'user',
targetKey: 'id'
});
Rating.belongsTo(models.Articles, {
foreignKey: 'articleId',
targetKey: 'id'
});
};
return Rating;
};
38 changes: 38 additions & 0 deletions src/middlewares/articleValidation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Joi from '@hapi/joi';

/**
* rating article validaton
*/
class ArticleValidation {
/**
* @param {object} req
* @param {object} res
* @param {object} next
* @returns {Object} ratingSchema object
*/
static async rating(req, res, next) {
const ratingSchema = Joi.object().keys({
rating: Joi.number().integer().valid('1', '2', '3', '4', '5').label('Rate')
.required()
});
const {
rating
} = req.body;
const rate = {
rating
};
const checkRating = Joi.validate(rate, ratingSchema, {
abortEarly: false
});
if (checkRating.error) {
const Errors = [];
for (let i = 0; i < checkRating.error.details.length; i += 1) {
Errors.push(checkRating.error.details[i].message.replace('"', ' ').replace('"', ' '));
}
return res.status(400).json({ Errors });
}
req.rate = checkRating.value;
next();
}
}
export default ArticleValidation;
4 changes: 4 additions & 0 deletions src/routes/api/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import checkUser from '../../middlewares/checkUser';
import { multerUploads } from '../../middlewares/multer';
import { cloudinaryConfig } from '../../db/config/cloudinaryConfig';

import articleRatingControllers from '../../controllers/articleRatingControllers';
import ArticleValidation from '../../middlewares/articleValidation';

const router = express.Router();
router.use('*', cloudinaryConfig);

Expand All @@ -13,5 +16,6 @@ router.delete('/article/:slug', auth.checkAuthentication, checkUser.isArticleOwn
router.get('/articles/:slug', articleController.readArticle);
router.put('/articles/:slug', auth.checkAuthentication, checkUser.isArticleOwner, multerUploads, articleController.updateArticle);
router.get('/articles', articleController.listAllArticles);
router.post('/articles/:slug/ratings', auth.checkAuthentication, ArticleValidation.rating, articleRatingControllers.rateArticle);

export default router;
63 changes: 63 additions & 0 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,19 @@
"type": "string"
}
}
},
"rating": {
"required": [
"rating"
],
"properties": {
"rating": {
"type": "integer"
},
"comment": {
"type": "string"
}
}
}
},
"paths": {
Expand Down Expand Up @@ -675,6 +688,56 @@
}
}
}
},
"/api/articles/{slug}/ratings":{
"post": {
"tags": [
"ratings"
],
"description": "User should be able to rate an article",
"parameters": [
{
"name": "token",
"in": "header",
"type": "string",
"required": true,
"description": "User Token"
},
{
"name": "slug",
"in": "path",
"required": true,
"type": "string",
"description": "Article Slug"
},
{
"name": "rating",
"in": "body",
"required": true,
"description": "Rating number",
"schema": {
"$ref": "#/definitions/rating"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Article rated successfuly"
},
"400": {
"description": "Rate must be one of [1, 2, 3, 4, 5]"
},
"404": {
"description": "Article not found"
},
"500": {
"description": "internal server error"
}
}
}
}
}
}
Loading

0 comments on commit d680356

Please sign in to comment.