Skip to content

Commit

Permalink
feat(share): user share article
Browse files Browse the repository at this point in the history
-user login
-user specifies the article's slug and platform to share on in the url
-platform is checked to be valid
-user is verified if they have alredy shared the article.
-if never, the share is made on the specified platform
-if they have shared the share is verified if it was on the specified platform
-if it was on the specified platform, the share is removed from the database and the user can create it again
-if it was not on the specified platform the share is created and added in the array of the previous share platforms
[WIP #165413120]
  • Loading branch information
Kabalisa committed May 20, 2019
1 parent 4049d88 commit 8e75ac0
Show file tree
Hide file tree
Showing 10 changed files with 657 additions and 1 deletion.
46 changes: 46 additions & 0 deletions controllers/article.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,52 @@ class ArticleController {
const result = await ArticleHelper.getDislikes(req);
return res.status(200).send({ dislikes: result, count: numberOfDislikes });
}

/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async shareArticle(req, res) {
const article = await ArticleHelper.findArticleBySlug(req.params.slug);
if (!article) { return res.status(404).send({ errors: { body: ['article not found'] } }); }
const result = await ArticleHelper.shareArticle(req);
if (result) {
const createdShare = await ArticleHelper.createShare(req);
return res.status(201).send({ share: createdShare });
}
return res.status(400).send({ errors: { body: ['share not created'] } });
}

/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async getShares(req, res) {
const { slug } = req.params;
const article = await ArticleHelper.findArticleBySlug(slug);
if (!article) { return res.status(404).send({ errors: { body: ['article not found'] } }); }
const shares = await ArticleHelper.getShares(slug);
if (shares.length < 1) {
return res.status(200).send({ message: 'this article has not been shared yet' });
}
const numberOfSharesOnPlatform = await ArticleHelper.numberOfSharesOnPlatform(shares);
const facebook = numberOfSharesOnPlatform[0];
const twitter = numberOfSharesOnPlatform[1];
const linkedin = numberOfSharesOnPlatform[2];
const gmail = numberOfSharesOnPlatform[3];
return res.status(200).send({
titleSlug: slug,
Shares: shares,
facebookShares: facebook,
twitterShares: twitter,
linkedinShares: linkedin,
gmailShares: gmail
});
}
}

export default ArticleController;
143 changes: 142 additions & 1 deletion helpers/article.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import slugify from 'slug';
import uniqid from 'uniqid';
import Joi from 'joi';
import open from 'open';
import PassportHelper from './passport';
import db from '../models';
import emitter from './eventEmitters';
import tagHelper from './tag.helper';

const {
Article, User, Tag, Like
Article, User, Tag, Like, Share
} = db;

/**
Expand Down Expand Up @@ -391,5 +392,145 @@ class ArticleHelper {
const tagList = await Tag.aggregate('name', 'DISTINCT', { plain: false }).map(row => row.DISTINCT);
return tagList || [];
}

/**
* function for checking if a platform is valid
* @function isShared
* @param {object} req
* @param {object} res
* @param {object} next
* @returns { string } appropriate message
*/
static async isPlatformValid(req, res, next) {
const { option } = req.params;
const platforms = ['facebook', 'twitter', 'gmail', 'linkedin'];
if (platforms.includes(option)) {
next();
return true;
}
return res.status(400).send({ errors: { body: ['invalid platform in path'] } });
}

/**
* function for checking if article is alreadry shared or not
* @function isShared
* @param {object} req
* @param {object} res
* @param {object} next
* @returns { string } appropriate message
*/
static async isShared(req, res, next) {
const { option } = req.params;
const { id } = req.user;
const share = await Share.findOne({ where: { userId: id } });
if (!share) { next(); return true; }
const { platform } = share;
if (platform.includes(option)) {
const updatePlatforms = platform.filter(result => result !== option);
await Share.update({ platform: updatePlatforms }, { where: { userId: id } });
return res.status(200).send({ message: `your ${option} share is removed, you can share again` });
}
next();
return true;
}

/**
* @param {object} req - Request object
* @returns {object} response
* @static
*/
static async shareArticle(req) {
const { option } = req.params;
const { slug } = req.params;
const { SHARE_URL } = process.env;
const articleShareUrl = `${SHARE_URL}${slug}`;
if (option === 'facebook') {
const result = await open(`http://www.facebook.com/sharer/sharer.php?u=${articleShareUrl}`);
return result;
} if (option === 'twitter') {
const result = await open(`https://twitter.com/intent/tweet?text=${articleShareUrl}`);
return result;
} if (option === 'gmail') {
const result = await open(`https://mail.google.com/mail/?view=cm&fs=1&tf=1&to=&su=Authorshaven%20Post&body=copy%20the%20following%20link%20to%20open%20the%20article%20${articleShareUrl}`);
return result;
} if (option === 'linkedin') {
const result = await open(`https://www.linkedin.com/sharing/share-offsite/?url=${articleShareUrl}`);
return result;
}
}

/**
* @param {object} req - Request object
* @returns {object} response
* @static
*/
static async createShare(req) {
const { option } = req.params;
const { slug } = req.params;
const { id } = req.user;
const share = await Share.findOne({ where: { userId: id } });
if (!share) {
await Share.create({
userId: id,
titleSlug: slug,
platform: [option],
createdAt: new Date(),
updatedAt: new Date()
});
const firstShare = await Share.findOne({ where: { userId: id } });
return firstShare;
}
const { platform } = share;
platform.push(option);
const newPlatform = platform;
await Share.update({
platform: newPlatform,
updatedAt: new Date()
}, { where: { userId: id } });
const createdShare = await Share.findOne({ where: { userId: id } });
return createdShare;
}

/**
* Return one article
* @param {object} slug - an object
*@return {object} Return shares
*/
static async getShares(slug) {
const shares = await Share.findAll({
where: { titleSlug: slug },
attributes: ['userId', 'platform', 'createdAt'],
raw: true
});
return shares;
}

/**
* Return one article
* @param {object} shares - an object
*@return {object} Return shares
*/
static async numberOfSharesOnPlatform(shares) {
let facebook = 0;
let twitter = 0;
let linkedin = 0;
let gmail = 0;
shares.forEach((share) => {
const Platform = share.platform;
if (Platform.includes('facebook')) {
facebook += 1;
}
if (Platform.includes('twitter')) {
twitter += 1;
}
if (Platform.includes('linkedin')) {
linkedin += 1;
}
if (Platform.includes('gmail')) {
gmail += 1;
}
});
return [facebook, twitter, linkedin, gmail];
}
}
export default ArticleHelper;
42 changes: 42 additions & 0 deletions migrations/20190518093044-create-share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const shareMigration = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Shares', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references: {
model: 'Users',
key: 'id'
}
},
titleSlug: {
type: Sequelize.STRING,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references: {
model: 'Articles',
key: 'slug'
}
},
platform: {
type: Sequelize.ARRAY(Sequelize.STRING),
defaultValue: ['neutral']
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Shares')
};
export default shareMigration;
4 changes: 4 additions & 0 deletions models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const articles = (sequelize, DataTypes) => {
foreignKey: 'articleId',
as: 'tagList'
});
Article.hasMany(models.Share, {
foreignKey: 'titleSlug',
sourceKey: 'slug'
});
};
return Article;
};
Expand Down
26 changes: 26 additions & 0 deletions models/share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const shares = (sequelize, DataTypes) => {
const Share = sequelize.define('Share', {
userId: DataTypes.INTEGER,
titleSlug: DataTypes.STRING,
platform: {
type: DataTypes.ARRAY(DataTypes.STRING),
defaultValue: ['neutral']
}
}, {});
Share.associate = (models) => {
Share.belongsTo(models.User, {
foreignKey: 'userId',
as: 'author',
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
});
Share.belongsTo(models.Article, {
foreignKey: 'titleSlug',
targetKey: 'slug',
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
});
};
return Share;
};
export default shares;
1 change: 1 addition & 0 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const users = (sequelize, DataTypes) => {
User.hasMany(models.Follows, { foreignKey: 'following' });
User.hasMany(models.Follows, { foreignKey: 'follower' });
User.hasMany(models.Notification, { as: 'user', foreignKey: 'userId' });
User.hasMany(models.Share, { foreignKey: 'userId' });
};
return User;
};
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-stub-transport": "^1.1.0",
"nodemon": "^1.18.11",
"open": "^6.3.0",
"passport": "^0.4.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
Expand Down
2 changes: 2 additions & 0 deletions routes/api/article/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ router.get('/:slug/likes', Auth, ArticleHelper.likesNumber, articleController.ge
router.get('/:slug/dislikes', Auth, ArticleHelper.dislikesNumber, articleController.getDislikes);
router.post('/:slug/tag', Auth, ArticleHelper.isOwner, TagMiddleware.isNotTagAdded, articleController.tagArticle);
router.get('/tag/list', articleController.getAllTags);
router.post('/:slug/share/:option', Auth, ArticleHelper.isPlatformValid, ArticleHelper.isShared, articleController.shareArticle);
router.get('/:slug/shares', Auth, articleController.getShares);

export default router;
72 changes: 72 additions & 0 deletions routes/api/article/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,75 @@
* '400':
* dislikes have been failed to be fetched
*/

/**
* @swagger
* /articles/{slug}/share/{option}:
* post:
* tags:
* - share
* name: share article
* summary: share an article on a platform specified in url
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: slug
* in: path
* schema:
* type: string
* required:
* - slug
* - name: option
* in: path
* schema:
* type: string
* required:
* - option
* - name: authorization
* in: header
* schema:
* type: string
* required:
* - authorization
* responses:
* '201':
* description: share is made succesfully
* '400':
* share has failed
* '404':
* article not found
*/

/**
* @swagger
* /articles/{slug}/shares:
* get:
* tags:
* - share
* name: get shares on an article
* summary: get shares on an article
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: slug
* in: path
* schema:
* type: string
* required:
* - slug
* - name: authorization
* in: header
* schema:
* type: string
* required:
* - authorization
* responses:
* '200':
* description: shares are fetched successfully
* '404':
* article or share not found
*/
Loading

0 comments on commit 8e75ac0

Please sign in to comment.