Skip to content

Commit

Permalink
feat(article): implement article readtime
Browse files Browse the repository at this point in the history
- Add wordsPerMinute column to the user model
- Add number of words column to the article model
- create function for calculating read time
- add beforeValidate hook to calculate noOfWords in the article model
- create get single article endpoint

[Finishes #159987415]
  • Loading branch information
tomiadebanjo committed Sep 21, 2018
1 parent 6c9af2f commit 5b585bf
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 16 deletions.
93 changes: 91 additions & 2 deletions server/controllers/articleController.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@

import slug from 'slug';
import uuid from 'uuid-random';
import ratingHelpers from '../helpers/ratingHelpers';
import models from '../models';
import { dataUri } from '../config/multer/multerConfig';
import imageUpload from '../helpers/imageUpload';
import readTime from '../helpers/readTime';
import tagManager from '../helpers/tagManager';

const {
Cases,
Articles,
Ratings,
ArticleLikes,
Tags
Tags,
Users,
Categories,
Comments
} = models;
const { analyseRatings } = ratingHelpers;

Expand Down Expand Up @@ -44,6 +47,7 @@ const articlesController = {
slug: `${slug(fields.title)}-${uuid()}`,
description: fields.description,
body: fields.body,
categoryId: parseInt(fields.categoryId, 10) || 1,
imageUrl
}).then((createdArticle) => {
// checks if tags exist
Expand Down Expand Up @@ -195,6 +199,91 @@ const articlesController = {
}
});
});
},
/**
* @description Get a single article
* @param {object} req The HTTP request object
* @param {object} res The HTTP response object
* @returns {object} Json response
*/
getSingleArticle: async (req, res) => {
const articleId = Number(req.params.articleId);
const currentUserId = Number(req.currentUser.id);

try {
const article = await Articles.findOne({
where: {
id: articleId
},
include: [{
model: Users,
attributes: ['id', 'image', 'username'],
}, {
model: Tags,
as: 'tags',
attributes: ['id', 'name'],
through: {
attributes: []
}
},
{
model: ArticleLikes,
as: 'articleLikes',
attributes: ['id', 'like', 'dislike']
},
{
model: Categories,
as: 'category',
attributes: ['name']
},
{
model: Comments,
as: 'comments',
attributes: ['id']
}]
});
const articleReadTime = await readTime(currentUserId, article);

const likes = article.articleLikes.filter(like => like.like === true).length;
const dislikes = article.articleLikes.filter(like => like.dislike === true).length;

const articleData = {
id: article.id,
slug: article.slug,
title: article.title,
body: article.body,
imageUrl: article.imageUrl,
rating: article.rating,
createdDate: article.createdAt,
updatedDate: article.updatedAt
};

const metadata = {
author: {
id: article.User.id,
username: article.User.username,
imageUrl: article.User.image
},
tags: article.tags,
likes,
dislikes,
readTime: articleReadTime,
commentCounts: article.comments.length,
category: article.category
};


return res.status(200).jsend.success({
message: 'Article found',
articleData,
metadata
});
} catch (error) {
res.status(404).jsend.fail({
message: 'Article not found',
error: error.message
});
}
}
};

Expand Down
20 changes: 20 additions & 0 deletions server/helpers/readTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import models from '../models';

const { Users } = models;
/**
* @description This method calculates the read of an article
* @param {integer} userId The id of the user getting the article
* @param {object} article The article object
* @returns {string} read time of an article
*/
const readTime = async (userId, article) => {
// Number of words in the article
const { noOfWords } = article.dataValues;
const user = await Users.findOne({ where: { id: userId } });
// number of words user can read per minute
const { wordsPerMinute } = user.dataValues;
const time = Math.ceil(noOfWords / wordsPerMinute);
return `${time} min read`;
};

export default readTime;
8 changes: 8 additions & 0 deletions server/migrations/20180919151449-userWordPerMinute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.addColumn('Users', 'wordsPerMinute', {
type: Sequelize.INTEGER,
defaultValue: 200
}),

down: queryInterface => queryInterface.removeColumn('Users', 'wordsPerMinute')
};
8 changes: 8 additions & 0 deletions server/migrations/20180919155337-noOfWords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.addColumn('Articles', 'noOfWords', {
type: Sequelize.INTEGER,
allowNull: false,
}),

down: queryInterface => queryInterface.removeColumn('Articles', 'noOfWords')
};
8 changes: 7 additions & 1 deletion server/models/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const articles = (sequelize, DataTypes) => {
rating: {
type: DataTypes.INTEGER,
allowNull: true
},
noOfWords: {
type: DataTypes.INTEGER
}
});

Expand Down Expand Up @@ -67,7 +70,10 @@ const articles = (sequelize, DataTypes) => {
as: 'ratings'
});
};

Articles.beforeValidate((article) => {
const text = article.body;
article.noOfWords = text.split(/\s/g).length;
});
return Articles;
};

Expand Down
4 changes: 4 additions & 0 deletions server/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const users = (sequelize, DataTypes) => {
},
interests: {
type: DataTypes.ARRAY(DataTypes.TEXT)
},
wordsPerMinute: {
type: DataTypes.INTEGER,
defaultValue: 200
}
});

Expand Down
12 changes: 9 additions & 3 deletions server/routes/articleRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const {
rateArticle,
create,
like,
getArticles
getArticles,
getSingleArticle
} = articleController;
const { addComment } = commentController;
const { validateArticle, validateComments } = inputValidator;
Expand All @@ -35,10 +36,14 @@ const {
} = reportValidation;

const articleRoutes = express.Router();
articleRoutes.get('/', auth, paginationParamsValidations, getArticles);
articleRoutes.get('/search', searchController);

// POST ARTICLE ROUTE
articleRoutes.get('/', auth, paginationParamsValidations, getArticles);
articleRoutes.post('/:articleId', auth, validArticleId, validateComments, addComment);
articleRoutes.post('/:articleId/comments', auth, validArticleId, validateComments, addComment);
articleRoutes.get('/:articleId', auth, checkParams.id, getSingleArticle);

articleRoutes.post(
'/:articleId/comments/like',
auth,
Expand All @@ -55,7 +60,7 @@ articleRoutes.post(
rateArticle
);
articleRoutes.post('/', auth, multerUploads, validateArticle, create);
articleRoutes.post('/:articleId/:likeType', auth, checkParams.id, checkParams.likeType, like);
articleRoutes.post('/:articleId/like/:likeType', auth, checkParams.id, checkParams.likeType, like);
articleRoutes.post(
'/:articleId/report/cases',
auth,
Expand All @@ -65,4 +70,5 @@ articleRoutes.post(
reportArticle
);
articleRoutes.get('/search', searchController);

export default articleRoutes;
12 changes: 12 additions & 0 deletions server/seeders/20180909082852-demo-article.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 5b585bf

Please sign in to comment.