Skip to content

Commit

Permalink
164796905-feature: Implement article reporting
Browse files Browse the repository at this point in the history
Complete implementation of article reporting

[#164796905]
  • Loading branch information
micah-akpan committed Apr 15, 2019
1 parent 978f95b commit aefdd57
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 44 deletions.
37 changes: 36 additions & 1 deletion src/controllers/article.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { slug } from '../utils/article';
import { Article } from '../models';
import { Article, Report, Sequelize } from '../models';

/**
* @name AddArticles
Expand Down Expand Up @@ -36,3 +36,38 @@ export const UpdateArticle = async () => true;
* @returns {int} Returns true after deleting an article
*/
export const DeleteArticle = async () => true;

/**
* @function reportArticle
* @param {Request} req The request object
* @param {Response} res The response object
* @returns {Object} Returns the server response
* @description This reports the article
*/
export const reportArticle = async (req, res) => {
try {
const { params, body, user } = req;
const newReport = await Report.create({
articleId: params.articleId,
reporterId: user.id,
reportCategory: body.category,
description: body.description
});
return res.status(200).json({ status: 'success', data: newReport });
} catch (error) {
if (error instanceof Sequelize.ForeignKeyConstraintError) {
return res.status(500).json({
status: 'error',
message: error.parent.detail
});
}
return res.status(500).json({
status: 'error',
message: 'Invalid request. Please check and try again'
});
}
};

export const getAllArticleReports = async (req, res) => res.status(404).send('Not yet implemented');

export const getASingleArticleReport = async (req, res) => res.status(404).send('Not yet implemented');
23 changes: 22 additions & 1 deletion src/middlewares/articles.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { validateArticle } from '../utils/article';
import {
validateArticle,
getArticleReportValidator
} from '../utils/article';

import { parseErrorResponse } from '../utils';

/**
*@name articleValidation
Expand All @@ -17,4 +22,20 @@ const articleValidation = async (req, res, next) => {
return next();
};

/**
* @function articleReportValidation
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @returns {Response} Returns a server response or call the next
* middleware function
*/
export const articleReportValidation = (req, res, next) => {
const validator = getArticleReportValidator(req.body);
if (validator.fails()) {
return res.status(400).json({ status: 'fail', data: parseErrorResponse(validator.errors.all()) });
}
return next();
};

export default articleValidation;
4 changes: 2 additions & 2 deletions src/migrations/20190331201917-create-article-rating.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default {
up: (queryInterface, Sequelize) => queryInterface.createTable('rating', {
up: (queryInterface, Sequelize) => queryInterface.createTable('article_ratings', {
id: {
allowNull: false,
primaryKey: true,
Expand Down Expand Up @@ -38,5 +38,5 @@ export default {
type: Sequelize.DATE,
},
}),
down: queryInterface => queryInterface.dropTable('rating'),
down: queryInterface => queryInterface.dropTable('article_ratings'),
};
17 changes: 15 additions & 2 deletions src/migrations/20190401140553-create-article-report.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default {
up: (queryInterface, Sequelize) => queryInterface.createTable('report', {
up: (queryInterface, Sequelize) => queryInterface.createTable('article_reports', {
id: {
allowNull: false,
primaryKey: true,
Expand All @@ -16,10 +16,23 @@ export default {
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
reporterId: {
type: Sequelize.UUID,
references: {
model: 'users',
key: 'id',
deferrable: Sequelize.Deferrable.INITIALLY_IMMEDIATE,
},
allowNull: false,
},
description: {
allowNull: false,
type: Sequelize.TEXT,
},
reportCategory: {
type: Sequelize.STRING,
defaultValue: 'others'
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
Expand All @@ -29,5 +42,5 @@ export default {
type: Sequelize.DATE,
},
}),
down: queryInterface => queryInterface.dropTable('report'),
down: queryInterface => queryInterface.dropTable('article_reports'),
};
2 changes: 1 addition & 1 deletion src/models/article_rating.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default (sequelize, DataTypes) => {
}
},
{
tableName: 'ratings',
tableName: 'article_ratings',
}
);
Rating.associate = (models) => {
Expand Down
10 changes: 9 additions & 1 deletion src/models/article_report.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@ export default (sequelize, DataTypes) => {
type: DataTypes.UUID,
allowNull: false,
},
reporterId: {
type: DataTypes.UUID,
allowNull: false,
},
reportCategory: {
type: DataTypes.STRING,
defaultValue: 'others'
},
description: {
allowNull: false,
type: DataTypes.TEXT,
},
},
{
tableName: 'report',
tableName: 'article_reports',
}
);
Report.associate = (models) => {
Expand Down
19 changes: 18 additions & 1 deletion src/routers/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Router } from 'express';
import passport from 'passport';
import passportAuth from '../middlewares/passport';
import { AddArticles, UpdateArticle, DeleteArticle } from '../controllers/article';
import {
AddArticles,
UpdateArticle,
DeleteArticle,
reportArticle,
getAllArticleReports,
getASingleArticleReport
} from '../controllers/article';
import { articleReportValidation } from '../middlewares/articles';
import checkFields from '../middlewares/auth/loginValidator';
import socialRedirect from '../controllers/authentication/socialRedirect';
import { login, createUser, linkedinUser, linkedinCallback } from '../controllers/authentication/user';
Expand Down Expand Up @@ -35,6 +43,15 @@ router
.delete(DeleteArticle)
.patch(UpdateArticle);

router
.route('/articles/:articleId/reports')
.post(articleReportValidation, Authenticator.verifyToken, reportArticle)
.get(getAllArticleReports);

router
.route('/api/v1/articles/:articleId/reports/:reportId')
.get(getASingleArticleReport);

router.post('/login', checkFields, passportAuth, login);

// Route for facebook Authentication
Expand Down
4 changes: 2 additions & 2 deletions src/seeders/20190402105309-article-rating.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default {
up: queryInterface => queryInterface.bulkInsert(
'rating',
'article_ratings',
[
{
id: '979eaa2e-5b8f-4103-8192-4639afae2bb9',
Expand All @@ -21,5 +21,5 @@ export default {
],
{}
),
down: queryInterface => queryInterface.bulkDelete('rating', null, {}),
down: queryInterface => queryInterface.bulkDelete('article_ratings', null, {}),
};
6 changes: 4 additions & 2 deletions src/seeders/20190402105400-article-report.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
export default {
up: queryInterface => queryInterface.bulkInsert(
'report',
'article_reports',
[
{
id: '979eaa2e-5b8f-4103-8192-4639afae2bb9',
articleId: '979eaa2e-5b8f-4103-8192-4639afae2ba8',
description: 'Plagiarism by Sanusi',
reporterId: '979eaa2e-5b8f-4103-8192-4639afae2ba9',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '979eaa2e-5b8f-4103-8192-4639afae2bb8',
articleId: '979eaa2e-5b8f-4103-8192-4639afae2ba7',
description: 'Plagiarism by Micah',
reporterId: '979eaa2e-5b8f-4103-8192-4639afae2ba9',
createdAt: new Date(),
updatedAt: new Date(),
},
],
{}
),
down: queryInterface => queryInterface.bulkDelete('report', null, {}),
down: queryInterface => queryInterface.bulkDelete('article_reports', null, {}),
};
14 changes: 14 additions & 0 deletions src/utils/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@ export const validateArticle = async (payload) => {
* @returns {string} Returns string
*/
export const slug = payload => slugify(payload, '-') + new Date().getTime();

export const getArticleReportValidator = (payload) => {
const rules = {
description: 'required|string|min:3',
category: 'string|min:3'
};
const errorMessages = {
'required.description': 'Please supply a :attribute of your report',
'string.description': 'Your :attribute field must be of string format',
'min.description': 'Your :attribute must be at least 3 characters long',
'string.category': 'Your :attribute field must be of string format'
};
return new Validator(payload, rules, errorMessages);
};
2 changes: 2 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const errorResponseFormat = (response) => {
* @function parseErrorResponse
* @param {object} responses
* @returns {object} Returns an hash of each field name to response messages
* @description This util function parses validation errors and returns them
* in a neater and more usable format
*/
export const parseErrorResponse = (responses) => {
const errorMessages = {};
Expand Down
71 changes: 71 additions & 0 deletions tests/integration/report.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'chai/register-should';
import chai from 'chai';
import chaiHttp from 'chai-http';
import { startServer } from '../../src/server';
import Auth from '../../src/middlewares/authenticator';

const user = {
id: '979eaa2e-5b8f-4103-8192-4639afae2ba8',
fullName: 'Martins Aloba',
role: 'admin',
username: 'martinsaloba'
};

chai.use(chaiHttp);

describe('Article Report API test', () => {
let app = null;
let agent = null;
beforeEach(async () => {
app = await startServer(6000);
agent = chai.request(app);
});
afterEach(async () => {
app = null;
agent = null;
});
it('should report an article', (done) => {
agent
.post('/api/v1/articles/979eaa2e-5b8f-4103-8192-4639afae2ba8/reports')
.set({ Authorization: Auth.generateToken(user) })
.send({ description: 'article contains plagiarized content', category: 'plagiarism' })
.end((err, res) => {
if (err) return done(err);
res.should.have.status(200);
res.body.status.should.equal('success');
res.body.data.description.should.equal('article contains plagiarized content');
res.body.data.reportCategory.should.equal('plagiarism');
done();
});
});

it('should return a database error for an article that doesn\'t exist', (done) => {
agent
.post('/api/v1/articles/979eaa2e-5b8f-4103-8192-4639afae2ba/reports')
.set({ Authorization: Auth.generateToken(user) })
.send({ description: 'article contains plagiarized content', category: 'plagiarism' })
.end((err, res) => {
if (err) return done(err);
res.should.have.status(500);
res.body.should.have.property('message');
res.body.status.should.equal('error');
done();
});
});

it('should return an error for an invalid request body', (done) => {
agent
.post('/api/v1/articles/979eaa2e-5b8f-4103-8192-4639afae2ba/reports')
.set({ Authorization: Auth.generateToken(user) })
.send({ category: 'plagiarism' })
.end((err, res) => {
if (err) return done(err);
res.should.have.status(400);
res.body.status.should.equal('fail');
res.body.should.have.property('data');
res.body.data.should.be.an('object');
res.body.data.description.should.equal('Please supply a description of your report');
done();
});
});
});
Loading

0 comments on commit aefdd57

Please sign in to comment.