Skip to content

Commit

Permalink
feature(user articles):
Browse files Browse the repository at this point in the history
- Implement handler that allows authenticated users fetch their own articles 
(#46)
  • Loading branch information
rafmme authored and seunkoko committed Jan 29, 2019
1 parent edf33d4 commit 916f083
Show file tree
Hide file tree
Showing 10 changed files with 438 additions and 58 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
"methods": "^1.1.2",
"mocha-lcov-reporter": "^1.3.0",
"morgan": "^1.9.1",
"node-boolify": "^1.0.5",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth20": "^1.0.0",
Expand Down
152 changes: 123 additions & 29 deletions server/controllers/ArticleController.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import pagination from '../helpers/pagination';
import TimeToRead from '../helpers/TimeToRead';
import ReadingStatsContoller from './ReadingStatsController';

const { Article, Tag, User } = db;
const {
Article,
Tag,
User,
Sequelize
} = db;
const { Op } = Sequelize;
const { createReadingStats } = ReadingStatsContoller;

/**
Expand Down Expand Up @@ -164,29 +170,33 @@ class ArticleController {
static async fetchOne(req, res) {
try {
const { slug } = req.params;
const userId = req.user !== undefined ? req.user.userId : null;

let article = await Article.findOne({
where: {
slug
slug,
[Op.or]: [{ isPublished: true }, { userId }],
},
attributes: { exclude: ['userId'] },
include: [
{
model: User,
as: 'author',
attributes: ['userName', 'bio', 'img']
},
{
model: Tag,
as: 'tags',
attributes: ['name'],
through: { attributes: [] }
}
include: [{
model: User,
as: 'author',
attributes: ['userName', 'bio', 'img'],
},
{
model: Tag,
as: 'tags',
attributes: ['name'],
through: { attributes: [] }
},
]
});

if (article) {
article = article.toJSON();
const tags = article.tags.map(tag => tag.name);
article.tags = tags;
article.timeToRead = TimeToRead.readTime(article);
article.createdAt = Util.formatDate(article.createdAt);
article.updatedAt = Util.formatDate(article.updatedAt);

Expand All @@ -195,30 +205,21 @@ class ArticleController {
}

return response(
res,
200,
'success',
res, 200, 'success',
'Article was fetched successfully',
null,
article
null, article
);
}
return response(
res,
404,
'failure',
res, 404, 'failure',
'not found error',
{ message: 'Article not found' },
null
{ message: 'Article not found' }
);
} catch (error) {
return response(
res,
500,
'failure',
res, 500, 'failure',
'server error',
{ message: 'Something went wrong on the server' },
null
{ message: 'Something went wrong on the server' }
);
}
}
Expand Down Expand Up @@ -248,6 +249,7 @@ class ArticleController {
req.body.slug = articleSlug;
let article = await result.update(req.body);
article = article.toJSON();
article.timeToRead = TimeToRead.readTime(article);
article.createdAt = Util.formatDate(article.createdAt);
article.updatedAt = Util.formatDate(article.updatedAt);
return response(
Expand Down Expand Up @@ -384,6 +386,98 @@ class ArticleController {
}
return res.redirect(socialShareLink);
}

/**
* @static
* @description this handles fetching of a particular articles for a user
* @param {object} req HTTP request object
* @param {object} res HTTP response object
* @returns {object} api route response with the articles
*/
static async fetchAllUserArticles(req, res) {
try {
const { userId } = req.user;
const {
tag,
drafts,
published,
page
} = req.query;

const limit = Number(req.query.limit) || 20;
const currentPage = Number(page) || 1;
const offset = (currentPage - 1) * limit;

const articlesCount = await Article.findAndCountAll({
where: { userId },
include: [
{
model: User,
as: 'author',
attributes: ['userName', 'bio', 'img']
},
],
});
const totalArticles = articlesCount.count;

const articles = await Article.findAndCountAll({
where: { userId },
include: [
{
model: User,
as: 'author',
attributes: ['userName', 'bio', 'img']
},
{
model: Tag,
as: 'tags',
attributes: ['name'],
through: { attributes: [] }
}
],
limit,
offset
});


if (articles.count > 0) {
let articleList = articles.rows.map((article) => {
article = article.toJSON();
article.timeToRead = TimeToRead.readTime(article);
article.tags = article.tags.map(articleTag => articleTag.name);
return article;
});

if (tag) {
articleList = ArticleHelper.filterAuthorArticle(articleList, 'tag', tag);
} else if (drafts === '') {
articleList = ArticleHelper.filterAuthorArticle(articleList, 'drafts');
} else if (published === '') {
articleList = ArticleHelper.filterAuthorArticle(articleList, 'published');
}

const paginatedData = pagination(articleList.length, limit, currentPage, totalArticles);
const data = {
articles: articleList,
paginatedData
};

return response(res, 200, 'success', 'All User articles', null, data);
}
return response(res, 200, 'success', 'All User articles', null, {
message: 'No articles posted yet'
});
} catch (error) {
return response(
res,
500,
'failure',
'server error',
{ message: 'Something went wrong on the server' },
null
);
}
}
}

export default ArticleController;
29 changes: 29 additions & 0 deletions server/helpers/ArticleHelper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import randomString from 'randomstring';

/**
* @class ArticleHelper
*/
Expand All @@ -19,6 +20,34 @@ class ArticleHelper {
}
}

/**
* @static
* @description a function for filtering an array of articles
* @param {array} arrayOfArticles
* @param {string} filteringOption { tag or drafts or published }
* @param {string} tagKeyword tag word
* @returns {array} returns the the filtered array
*/
static filterAuthorArticle(arrayOfArticles, filteringOption, tagKeyword) {
let filteredArray = [];
switch (filteringOption) {
case 'tag':
filteredArray = arrayOfArticles
.filter(articleArray => articleArray.tags
.map(tag => tag.toLowerCase())
.indexOf(tagKeyword.toLowerCase()) !== -1);
return filteredArray;
case 'drafts':
filteredArray = arrayOfArticles.filter(articleArray => articleArray.isPublished === false);
return filteredArray;
case 'published':
filteredArray = arrayOfArticles.filter(articleArray => articleArray.isPublished === true);
return filteredArray;
default:
return arrayOfArticles;
}
}

/**
* @static
* @description a function for generating social share link
Expand Down
10 changes: 4 additions & 6 deletions server/middlewares/validations/ArticleValidation.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/* eslint-disable no-useless-escape */
/* eslint-disable no-unused-expressions */
/* eslint-disable prefer-const */

import { Boolify } from 'node-boolify';
import db from '../../models';
import Util from '../../helpers/Util';
import response from '../../helpers/response';
Expand Down Expand Up @@ -165,9 +163,9 @@ class ArticleValidation {
req.body.banner = req.body.banner
? Util.removeExtraWhitespace(req.body.banner) : article.banner;
req.body.isPublished = req.body.isPublished !== undefined
? Boolify(req.body.isPublished) : article.isPublished;
? Boolean(req.body.isPublished) : article.isPublished;
req.body.isReported = req.body.isReported !== undefined
? Boolify(req.body.isReported) : article.isReported;
? Boolean(req.body.isReported) : article.isReported;
return next();
}
return response(
Expand Down Expand Up @@ -200,7 +198,7 @@ class ArticleValidation {
return next();
}
if (!Number.isSafeInteger(parseInt(limit, 10))) {
response(res, 400, 'failure', 'There was an issue with your query');
return response(res, 400, 'failure', 'There was an issue with your query');
}
return next();
} catch (error) {
Expand Down Expand Up @@ -241,7 +239,7 @@ class ArticleValidation {
'server error',
{
message: 'Something went wrong on the server'
}, null
}
);
}
}
Expand Down
20 changes: 15 additions & 5 deletions server/routes/api/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import UserController from '../../controllers/UserController';
import followController from '../../controllers/followController';
import AuthMiddleware from '../../middlewares/AuthMiddleware';
import handleValidationErrors from '../../middlewares/validations/handleValidationErrors';
import {
signUpSchema,
logInSchema,
editProfileSchema
} from '../../middlewares/validations/userValidation';
import { signUpSchema, logInSchema, editProfileSchema } from '../../middlewares/validations/userValidation';
import ArticleController from '../../controllers/ArticleController';
import ArticleValidation from '../../middlewares/validations/ArticleValidation';

const userRoutes = Router();

Expand Down Expand Up @@ -56,5 +54,17 @@ userRoutes.delete(
AuthMiddleware.checkIfUserIsAuthenticated,
followController.unfollowUser
);
userRoutes.get(
'/myArticles',
AuthMiddleware.checkIfUserIsAuthenticated,
ArticleValidation.verifyLimitParams,
ArticleValidation.verifyPageParams,
ArticleController.fetchAllUserArticles
);
userRoutes.get(
'/myArticles/:slug',
AuthMiddleware.checkIfUserIsAuthenticated,
ArticleController.fetchOne
);

export default userRoutes;
8 changes: 6 additions & 2 deletions server/seeders/20190115151310-demoUser.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@

export default {
up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Users', [{
id: '45745c60-7b1a-11e8-9c9c-2d42b21b1a3e',
fullName: 'Jesse',
userName: 'jesseinit',
email: 'jesseinit@now.com',
bio: 'Gitting Started',
isVerified: true,
password: '$2y$10$z7F4f33h3XJvw/ke6ncO3uY1KdFQJb0.pcwhVh5BRgdfyc0Itlz/i',
authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e'
}, {
Expand All @@ -13,7 +15,8 @@ export default {
userName: 'kabir',
email: 'kabir@now.com',
bio: 'Learning life now',
password: '$2y$10$QCQ1uW0OWH7xKOvJ9gNWsewzoXSjvAmXw21mcZBEB52TN6T/f2Xfy',
isVerified: true,
password: 'Blahblah',
authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e'
},
{
Expand All @@ -22,7 +25,8 @@ export default {
userName: 'steve',
email: 'steve@now.com',
bio: 'Gitting Started',
password: '$2y$10$5hj02gtnG2xxYHqpkrqeXOs3kj0t3uTKUMqdKBYGeHOKRNrZdTT9O',
password: 'Blahblah',
isVerified: false,
authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e'
}
], {}),
Expand Down
Loading

0 comments on commit 916f083

Please sign in to comment.