Skip to content

Commit

Permalink
Merge pull request #57 from andela/ft-reading-stats-improve-166536360
Browse files Browse the repository at this point in the history
#166536360 Improve article reading statistics
  • Loading branch information
Denis Niwemugisha committed Jun 11, 2019
2 parents c7121d5 + f35a985 commit f9123ac
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 26 deletions.
6 changes: 4 additions & 2 deletions controllers/article.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ class ArticleController {
articleId, userId
});

if (!isArticleAuthor && (!hasRead || !hasRead.userId)) {
await statsHelper.saveReadingStats(userId, articleId);
if (!isArticleAuthor && !hasRead) {
await statsHelper.saveReadingStats({ userId, articleId, isDuplicate: false });
} else if (!isArticleAuthor && (hasRead)) {
await statsHelper.saveReadingStats({ userId, articleId, isDuplicate: true });
}

article.dataValues.tagList = tags;
Expand Down
98 changes: 75 additions & 23 deletions helpers/read.stats.helper.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import models from '../models';

const {
Article, ReadingStat, User, Sequelize
Article, ReadingStat, User, Sequelize, Comment, Like, Share
} = models;
/**
* @class ReadStatsHelper
Expand All @@ -10,15 +10,16 @@ const {
class ReadStatsHelper {
/**
* @function saveReadingStats
* @param {number} userId - User ID
* @param {number} articleId - Article ID
* @param {object} statsInfo - User ID
* @returns {object} object of article statistics that created
* @static
*/
static async saveReadingStats(userId, articleId) {
static async saveReadingStats(statsInfo) {
const { articleId, userId, isDuplicate } = statsInfo;
const newReadStat = await ReadingStat.create({
articleId,
userId
userId,
isDuplicate
});
return newReadStat;
}
Expand All @@ -37,6 +38,35 @@ class ReadStatsHelper {
return statsList.length;
}

/**
* @function getArticleReaders
* @param {array} duplicateReaders - Article ID
* @returns {array} - Array of users
* @static
*/
static async getArticleReaders(duplicateReaders) {
const readers = [];
const nonDuplicates = [];
duplicateReaders.forEach((reader) => {
const { userId } = reader.dataValues;
const index = nonDuplicates.indexOf(userId);
if (index === -1) nonDuplicates.push(userId);
});
await Promise.all(nonDuplicates.map(async (reader) => {
const userInfo = await User.findOne({
where: { id: reader },
attributes: ['firstName', 'lastName', 'username']
});
readers.push({
id: reader,
firstName: userInfo.dataValues.firstName,
lastName: userInfo.dataValues.lastName,
username: userInfo.dataValues.username
});
}));
return readers;
}

/**
* @function articlesStats
* @param {number} userId - User ID
Expand All @@ -46,30 +76,51 @@ class ReadStatsHelper {
static async getArticlesStats(userId) {
const articles = await Article.findAll({
where: { authorId: userId },
attributes: ['id', 'title']
attributes: ['id', 'title', 'slug'],
logging: false
});
const allReaders = await this.getAllData('userId');
const stats = [];
await Promise.all(articles.map(async (currentArticle) => {
const { id } = currentArticle.dataValues;
const readStats = await ReadingStat.findAndCountAll({
const { id, slug } = currentArticle.dataValues;
const registered = await ReadingStat.findAndCountAll({
where: { articleId: id, userId: { [Sequelize.Op.ne]: null } },
attributes: [],
include: [{ model: User, attributes: ['username', 'bio', 'image'] }]
logging: false
});

const notRegistered = await ReadingStat.count({
where: { articleId: id, userId: null },
logging: false
});

const notDuplicates = await ReadingStat.count({
where: { articleId: id, isDuplicate: false },
logging: false
});
const readPercantage = ((Number(readStats.count) / allReaders) * 100) || 0;
const duplicates = await ReadingStat.count({
where: { articleId: id, isDuplicate: true },
logging: false
});
const totalComments = await Comment.count({ where: { titleSlug: slug }, logging: false });
const totalLikes = await Like.count({ where: { titleSlug: slug, status: 'like' }, logging: false });
const totaldDisLikes = await Like.count({ where: { titleSlug: slug, status: 'dislike' }, logging: false });
const totalShares = await Share.count({ where: { titleSlug: slug }, logging: false });

currentArticle.dataValues.notRegistered = notRegistered;
currentArticle.dataValues.registered = readStats.count;
currentArticle.dataValues.percentage = readPercantage.toFixed(2);
currentArticle.dataValues.readers = readStats.rows;
const totalViews = notDuplicates + duplicates;
const readers = await this.getArticleReaders(registered.rows);

return currentArticle;
stats.push({
id,
slug,
notRegistered,
registered: registered.count,
totalViews,
totalComments,
totalLikes,
totaldDisLikes,
totalShares,
readers
});
}));
return articles;
return stats;
}

/**
Expand All @@ -79,23 +130,24 @@ class ReadStatsHelper {
* @static
*/
static async getReadingStats(userId) {
const stats = await ReadingStat.findAll({
let stats = await ReadingStat.findAll({
where: { userId },
include: [{
model: Article, attributes: ['title']
}],
attributes: ['articleId', 'createdAt']
});
const allArticles = await this.getAllData('articleId');
stats = stats.filter((stat, index, self) => index === self.findIndex(t => (
t.dataValues.articleId === stat.dataValues.articleId
)));
const articles = [];
await Promise.all(stats.map(async (currentStat) => {
const { articleId } = currentStat.dataValues;
const { title } = currentStat.dataValues.Article;

articles.push({ articleId, title });
}));
const percentage = ((articles.length * 100) / allArticles) || 0;
return { articles, percentage };
return articles;
}
}

Expand Down
8 changes: 8 additions & 0 deletions migrations/20190607072658-add-stats-duplicate-column.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const statsDuplicate = {
up: (queryInterface, Sequelize) => queryInterface.addColumn('ReadingStats', 'isDuplicate', {
type: Sequelize.BOOLEAN,
defaultValue: false
}),
down: queryInterface => queryInterface.removeColumn('ReadingStats', 'isDuplicate')
};
export default statsDuplicate;
4 changes: 4 additions & 0 deletions models/readingstat.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ const readingStats = (sequelize, DataTypes) => {
const ReadingStat = sequelize.define('ReadingStat', {
articleId: DataTypes.INTEGER,
userId: DataTypes.INTEGER,
isDuplicate: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
}, {});
ReadingStat.associate = (models) => {
// associations can be defined here
Expand Down
2 changes: 1 addition & 1 deletion tests/read.stats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('/api/v1/articles/reading/statistics. Article reading statistics', () =
.set('authorization', readerToken)
.end((err, res) => {
expect(res.body).to.have.status(200);
expect(res.body.readStats).to.be.an('object');
expect(res.body.readStats).to.be.an('array');
done();
});
});
Expand Down

0 comments on commit f9123ac

Please sign in to comment.