Skip to content

Commit

Permalink
feat(article): Add read time functionality (#30)
Browse files Browse the repository at this point in the history
- add read time method
- add read time attribute
- return read time when get article

[Finishes #166789888]
  • Loading branch information
j4l13n authored and emukungu committed Jul 19, 2019
1 parent 4fd0399 commit 469708d
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 99 deletions.
88 changes: 24 additions & 64 deletions src/controllers/articleController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import model from '../db/models/index';
import { dataUri } from '../middlewares/multer';
import { uploader } from '../db/config/cloudinaryConfig';
import readTime from '../helpers/readTime';

const { Users, Articles } = model;
/**
Expand Down Expand Up @@ -30,10 +31,10 @@ class articleManager {
body,
description,
image,
postedBy: id,
authorId: id,
tagList,
author: req.user,
slug: generateSlug.toLowerCase()
slug: generateSlug.toLowerCase(),
readtime: readTime.read(body),
};
const postNewArticle = await Articles.create(newArticle);
if (!postNewArticle.error) {
Expand Down Expand Up @@ -80,37 +81,20 @@ class articleManager {
*/
static async readArticle(req, res) {
try {
const { slug } = req.params;
const findArticle = await Articles.findOne({
where: { slug: req.params.slug }
where: { slug },
include: [{
as: 'author',
model: Users,
attributes: ['username', 'bio', 'image']
}],
attributes: ['slug', 'title', 'description', 'readtime', 'body', 'tagList', 'favorited', 'favoritesCount', 'updatedAt', 'createdAt']
});

const { title, body, description, favoritesCount,
slug, createdAt, updatedAt, favorited, tagList, postedBy } = findArticle.dataValues;

const findAuthor = await Users.findOne({
where: { id: postedBy }
return res.status(200).json({
article: findArticle
});
if (findArticle) {
return res.status(200).json({
article: {
slug,
title,
description,
body,
tagList,
createdAt,
updatedAt,
favorited,
favoritesCount,
author: {
username: findAuthor.username,
bio: findAuthor.bio,
image: findAuthor.image,
following: findAuthor.following
}
}
});
}
} catch (error) {
return res.status(404).json({
error: 'article not found',
Expand All @@ -131,12 +115,7 @@ class articleManager {
const result = await uploader.upload(file);
req.body.image = result.url;
}
const {
title,
description,
body,
image,
} = req.body;
const { title, description, body, image, } = req.body;

const oldArticle = req.article;
const updateArticle = await oldArticle.update({
Expand Down Expand Up @@ -165,35 +144,16 @@ class articleManager {
*/
static async listAllArticles(req, res) {
try {
const articlesList = await Articles.findAll({ include: [Users] });
const cacheArticles = [];
for (let i = 0; i < articlesList.length; i += 1) {
const {
slug, title, description, body, tagList, createdAt, updatedAt, favorited, favoritesCount
} = articlesList[i];

const { User } = articlesList[i];
const {
username, bio, image, following
} = User;

const author = { username, bio, image, following };

const art = { slug,
title,
description,
body,
tagList,
createdAt,
updatedAt,
favorited,
favoritesCount,
author
};
cacheArticles.push(art);
}
const articlesList = await Articles.findAll({
include: [{
as: 'author',
model: Users,
attributes: ['username', 'bio', 'image']
}],
attributes: ['id', 'slug', 'title', 'description', 'readtime', 'body', 'tagList', 'updatedAt', 'createdAt']
});
return res.status(200).json({
articles: cacheArticles
articles: articlesList
});
} catch (error) {
return res.status(404).json({
Expand Down
9 changes: 6 additions & 3 deletions src/db/migrations/20190708174940-create-articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ module.exports = {
type: Sequelize.INTEGER,
defaultValue: 0
},
readtime: {
type: Sequelize.STRING,
allowNull: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
Expand All @@ -45,14 +49,13 @@ module.exports = {
allowNull: false,
type: Sequelize.DATE
},
postedBy: {
authorId: {
type: Sequelize.INTEGER,
allowNull: false,
onDelete: 'CASCADE',
references: {
model: 'Users',
key: 'id',
as: 'postedBy'
key: 'id'
}
}
});
Expand Down
27 changes: 24 additions & 3 deletions src/db/models/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,33 @@ export default (sequelize, DataTypes) => {
type: DataTypes.INTEGER,
defaultValue: 0
},
image: DataTypes.BLOB,
slug: DataTypes.STRING,
image: {
type: DataTypes.BLOB,
allowNull: {
args: true,
}
},
slug: {
type: DataTypes.STRING,
allowNull: {
args: true
}
},
readtime: {
type: DataTypes.STRING,
allowNull: {
args: true
}
},
authorId: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {});
Articles.associate = function(models) {
Articles.belongsTo(models.Users, {
foreignKey: 'postedBy',
as: 'author',
foreignKey: 'authorId',
onDelete: 'CASCADE'
});
Articles.hasMany(models.Rating, {
Expand Down
3 changes: 2 additions & 1 deletion src/db/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export default (sequelize, DataTypes) => {
Users.associate = function(models) {
// associations can be defined here
Users.hasMany(models.Articles, {
foreignKey: "postedBy"
as: 'author',
foreignKey: "authorId"
});
Users.hasMany(models.Followers, {
foreignKey: "follower",
Expand Down
4 changes: 2 additions & 2 deletions src/db/seeders/20190710155910-Articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
title: 'TIA',
body: 'This is Andela',
description: 'From the heart and deep on the soul of young African software engineers',
postedBy: 1,
authorId: 1,
slug: 'TIA',
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
Expand All @@ -17,7 +17,7 @@ module.exports = {
title: 'Delete this',
body: 'This is Andela',
description: 'From the heart and deep on the soul of young African software engineers',
postedBy: 1,
authorId: 1,
slug: 'dropTIA',
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
Expand Down
40 changes: 40 additions & 0 deletions src/helpers/readTime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable class-methods-use-this */
/**
* Read Time class
*/
class ReadTime {
/**
*
* @param {Integer} seconds
* @returns {String} minutes
*/
convert(seconds) {
if (seconds > 60) {
return `${Math.ceil(seconds / 60)} min`;
}
return 'Less than a minute';
}

/**
*
* @param {String} words
* @returns {Integer} number of words
*/
wordsLength(words) {
return words.split(' ').length;
}

/**
*
* @param {String} text
* @returns {Integer} seconds
*/
read(text) {
const wordPerSecond = 4;
const words = this.wordsLength(text);
const seconds = words / wordPerSecond;
return this.convert(seconds);
}
}

export default new ReadTime();
4 changes: 1 addition & 3 deletions src/middlewares/articleValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ class ArticleRatingValidation {
rating: Joi.number().integer().valid('1', '2', '3', '4', '5').label('Rate')
.required()
});
const {
rating
} = req.body;
const { rating } = req.body;
const rate = {
rating
};
Expand Down
2 changes: 1 addition & 1 deletion src/middlewares/checkUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class CheckUse {
const findArticle = await Articles.findOne({
where: { slug: req.params.slug }
});
if (findArticle.dataValues.postedBy !== req.user.id) {
if (findArticle.dataValues.authorId !== req.user.id) {
return res.status(403).json({
message: 'you are not allowed to perfom this action'
});
Expand Down
11 changes: 1 addition & 10 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -600,15 +600,6 @@
"type": "string",
"description": "a slug that uniquelly identifies an article",
"require": true
},
{
"name": "token",
"in": "header",
"description": "a token for authentication and authorization",
"require": true,
"schema": {
"$ref": "#/definitions/display-specific-article"
}
}
],
"produces": [
Expand Down Expand Up @@ -864,7 +855,7 @@
"/api/users/profile": {
"get": {
"tags": [
"users"
"profiles"
],
"description": "Get all authors profiles",
"parameters": [
Expand Down
3 changes: 3 additions & 0 deletions tests/.codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exclude_patterns:
- "src/db/"
- "tests/"
12 changes: 0 additions & 12 deletions tests/articlesTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,6 @@ describe('Article test', () => {
done();
});

it('should display a specific articles', (done) => {
chai.request(index)
.get('/api/articles/TIA')
.set('token', userToken)
.end((err, res) => {
res.body.should.be.an('object');
res.body.should.have.property('article');
res.body.article.should.be.an('object');
});
done();
});

it('should update an existing article', (done) => {
const article = {
title: 'This is wonderful',
Expand Down
31 changes: 31 additions & 0 deletions tests/readTimeTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect } from 'chai';
import readTime from '../src/helpers/readTime';


describe('Read time tests', () => {
it('should return a beautiful read time', () => {
const text = `Since joining Andela’s Bootcamp,
that was the first time to know how to test my codes,
because there are some situations where the software( like a website)
is being broken in production(means when the users are using that product and face the technical bugs).
The way this was a challenge to me is that I had to learn it fast and implement them immediately.
I found how important it is, the way you target every block of code as input and
expect each possible output in order to catch some errors and correct them before the product
is going to get deployed. The main thing I learned from this challenge is that
I have to make sure my codes are bug-free before getting deployed and make sure my tests are covering every
block of codes. How I adapted to this challenge is, I spent a lot of sleepless nights figuring out how
to write my tests, I didn’t know anything about Travis CI and I got several emails that my builds were failing.
The main key is working hard, ask around and do more research to get your work done.\nGit workflow was another challenge I faced.
I tried it before, but I never tried the feature-branch workflow.
I found that workflow was awesome because it helps you manage the project tasks and work them into branches.
The reason it was a challenge is that we had to work in several branches and merge our work into the main branch and sometimes you face some merge conflicts.
I didn’t know how to resolve conflicts, but I tried to make some research about it, ask my colleagues how to resolve them and luckily
I got my work really organized on Github.`;
expect(readTime.read(text)).to.equal('2 min');
});

it('should get a less than a minute read time', () => {
const text = 'This is an amazing project we are working on, Authors Haven';
expect(readTime.read(text)).to.equal('Less than a minute');
});
});

0 comments on commit 469708d

Please sign in to comment.