Skip to content

Commit

Permalink
feat(reading-stats): users should be able to see their reading stats
Browse files Browse the repository at this point in the history
- create an endpoint to enable user to create and retrieve stats
- unit test and swagger documentation of the feature

[Finishes #166789894]
  • Loading branch information
Dom58 committed Aug 1, 2019
1 parent 34bda0d commit 4a57789
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 8 deletions.
60 changes: 60 additions & 0 deletions src/controllers/statisticController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import model from '../db/models/index';

const { Articles, Statistics } = model;
/**
* statistic controller
*/
class StatisticManager {
/**
* @param {object} req
* @param {object} res
* @returns {object} stats
*/
static async createStats(req, res) {
const { id } = req.user;
const { slug } = req.params;
const findArticle = await Articles.findOne({ where: { slug } });
if (!findArticle) return res.status(200).json({ error: 'No article found' });
const [result, created] = await Statistics.findOrCreate({
where: { userId: id, articleId: findArticle.id }, defaults: { numberOfReading: 1 }
});
if (!created) {
await Statistics.update(
{ numberOfReading: result.numberOfReading + 1 },
{ where: { id: result.id }, returning: true }
);
}
return res.status(201).json({
message: 'I am reading an article...',
On: result.updatedAt,
article: findArticle.title
});
}

/**
* @param {object} req
* @param {object} res
* @returns {object} retrieve all user stats
*/
static async retrieveUserReadingStats(req, res) {
const { id } = req.user;
const countTotalArticlesRead = await Statistics.count({ where: { userId: id } });
const articlesRead = await Statistics.findAll({
where: { userId: id },
attributes: [
['numberOfReading', 'TimesArticleRead'],
['updatedAt', 'lastSeen']],
include: [
{
model: Articles,
attributes: ['title', 'slug', 'body'],
}
]
});
if (countTotalArticlesRead) {
return res.status(200).json({ allArticlesRead: countTotalArticlesRead, articlesRead });
}
return res.status(200).json({ allArticlesRead: 0, articlesRead });
}
}
export default StatisticManager;
47 changes: 47 additions & 0 deletions src/db/migrations/20190731191521-create-statistics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Statistics', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID
},
userId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'Users',
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
articleId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'Articles',
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
numberOfReading: {
allowNull: false,
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Statistics');
}
};
33 changes: 33 additions & 0 deletions src/db/models/statistics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';
module.exports = (sequelize, DataTypes) => {
const Statistics = sequelize.define('Statistics', {
id: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4
},
userId: {
type: DataTypes.INTEGER,
allowNull: true,
},
articleId: {
type: DataTypes.INTEGER,
allowNull: true
},
numberOfReading: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 0
}
}, {});
Statistics.associate = function(models) {
Statistics.belongsTo(models.Users, {
foreignKey: 'userId'
});
Statistics.belongsTo(models.Articles, {
foreignKey: 'articleId'
});
};
return Statistics;
};
16 changes: 8 additions & 8 deletions src/db/seeders/20190723161730-create-permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ module.exports = {
return queryInterface.bulkInsert('Permissions', [{
role: 'admin',
action: 'POST',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings' ],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings', 'Statistics' ],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
{
role: 'admin',
action: 'PUT',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings' ],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings', 'Statistics' ],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
{
role: 'admin',
action: 'GET',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings'],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings', 'Statistics'],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
{
role: 'admin',
action: 'DELETE',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings' ],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings', 'Statistics' ],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
Expand All @@ -36,28 +36,28 @@ module.exports = {
{
role: 'normal',
action: 'POST',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings' ],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'likeDislikes', 'Reportings', 'Statistics'],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
{
role: 'normal',
action: 'PUT',
resources: ['Articles', 'Users', 'Comments', 'Permissions', 'Tags'],
resources: ['Articles', 'Users', 'Comments', 'Permissions', 'Tags', 'Statistics'],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
{
role: 'normal',
action: 'GET',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags'],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'Statistics'],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
},
{
role: 'normal',
action: 'DELETE',
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags'],
resources: ['Articles', 'Users', 'Comments', 'Followers', 'Permissions', 'Ratings', 'Tags', 'Statistics'],
createdAt: moment.utc().format(),
updatedAt: moment.utc().format()
}
Expand Down
20 changes: 20 additions & 0 deletions src/middlewares/checkUserPermissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ class UserPermissions {
next();
} else return res.status(403).json({ error: 'access denied' });
}

/**
*
* @param {object} req
* @param {object} res
* @param {object} next
* @returns {0} -
*/
static async checkStatisticsPermissions(req, res, next) {
const { method } = req;
const checkIfAllowed = await checkPermissions.isAllowed(req.user.role, 'Statistics', method);
if (checkIfAllowed) {
/**
*
* go next if user is allowed
*
*/
next();
} else return res.status(403).json({ error: 'access denied' });
}
}

export default UserPermissions;
3 changes: 3 additions & 0 deletions src/routes/api/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import articleRatingControllers from '../../controllers/articleRatingControllers
import articleValidation from '../../middlewares/articleValidation';
import isUserAllowed from '../../middlewares/checkUserPermissions';
import searchController from '../../controllers/searchController';
import statisticController from '../../controllers/statisticController';

const router = express.Router();
// statistics route
router.post('/stats/:slug/save-reading', auth.checkAuthentication, isUserAllowed.checkStatisticsPermissions, statisticController.createStats);

// routes that don't need authentication
router.get('/', articleController.listAllArticles);
Expand Down
3 changes: 3 additions & 0 deletions src/routes/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import isAuth from '../../middlewares/isAuth';
import { multerUploads } from '../../middlewares/multer';
import { cloudinaryConfig } from '../../db/config/cloudinaryConfig';
import checkUser from '../../middlewares/checkUser';
import statisticController from '../../controllers/statisticController';


const { logoutToken } = logout;
Expand Down Expand Up @@ -41,4 +42,6 @@ router.use('/', auth.checkAuthentication, isUserAllowed.checkUsersPermissions);
router.get('/profiles', profileController.getAllUsersProfile);
router.put('/profile/:username', isAuth.isOwner, logoutToken, multerUploads, profileController.updateProfile);

// Authentication user should be able to read his stats
router.get('/reading-stats', isUserAllowed.checkStatisticsPermissions, statisticController.retrieveUserReadingStats);
export default router;
72 changes: 72 additions & 0 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@
"type":"boolean"
}
}
},
"reading-stats": {
"required": [
"token",
"slug"
]
}
},
"paths": {
Expand Down Expand Up @@ -2186,6 +2192,72 @@
}
}
}
},
"/api/articles/stats/{slug}/save-reading": {
"post": {
"tags": [
"Reading Stats"
],
"description": "Reading Statistic",
"parameters": [
{
"name": "token",
"in": "header",
"description": "user token",
"required": true,
"type": "string"
},
{
"name": "slug",
"in": "path",
"description": "Article slug",
"required": true,
"type": "string",
"schema": {
"$ref": "#/definitions/rreading-stats"
}
}
],
"produces": [
"application/json"
],
"responses": {
"201": {
"description": "I am reading an article..."
},
"404": {
"description": "Article to read not found"
}
}
}
},
"/api/users/reading-stats": {
"get": {
"tags": [
"Reading Stats"
],
"description": "Retrieve article reading stats",
"parameters": [
{
"name": "token",
"in": "header",
"description": "user token",
"required": true,
"type": "string",
"schema": {
"$ref": "#/definitions/rreading-stats"
}
}
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "Get my Stat(s)"
}
}
}
}
}
}
Loading

0 comments on commit 4a57789

Please sign in to comment.