Skip to content
This repository has been archived by the owner on Jul 20, 2020. It is now read-only.

Commit

Permalink
feature(search): user search for a request
Browse files Browse the repository at this point in the history
- add the search controller
- add test for the search functionality
- add the validation rule middleware
- document the search feature

[Maintains #170947564]
  • Loading branch information
izzett222 committed Mar 3, 2020
1 parent 9f5a537 commit 9c302c2
Show file tree
Hide file tree
Showing 18 changed files with 352 additions and 36 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@
"jsonwebtoken": "^8.5.1",
"localStorage": "^1.0.4",
"mailgen": "^2.0.10",
"multer": "^1.4.2",
"passport": "^0.4.1",
"passport-facebook": "^3.0.0",
"passport-google-oauth20": "^2.0.0",
"multer": "^1.4.2",
"path": "^0.12.7",
"pg": "^7.18.1",
"pg-hstore": "^2.3.3",
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default class AuthController {
const token = provideToken(user.id, user.isVerified, email, user.role);
const link = `http://${process.env.BASE_URL}/api/v1/auth/verification?token=${token}&email=${email}`;
localStorage.setItem('token', token);
sendMsg(email, firstName, content, link);
await sendMsg(email, firstName, content, link);
return Response.signupResponse(res, 201, res.__('User is successfully registered'), token);
} catch (error) {
return Response.errorResponse(res, 500, res.__(`${error.message}`));
Expand Down Expand Up @@ -182,7 +182,7 @@ export default class AuthController {
text: req.__('button text'),
signature: req.__('email signature')
};
AuthService.forgotPassword(user, content);
await AuthService.forgotPassword(user, content);
return Response.success(res, 200, res.__('check your email to reset your password'));
} catch (err) {
return Response.errorResponse(res, 500, res.__('server error'));
Expand Down
41 changes: 38 additions & 3 deletions src/controllers/tripsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import uuid from 'uuid/v4';
import db from '../models';
import Response from '../utils/ResponseHandler';
import findFacilityHandlder from '../utils/findFacility';
import TripsService from '../services/tripServices';

/**
*
* @description RequestController Controller
Expand Down Expand Up @@ -49,7 +51,7 @@ export default class requestController {
passportName,
role
}],
status: 'open',
status: 'open'
});
return Response.success(res, 201, 'Request created successfully', newRequest);
}
Expand Down Expand Up @@ -107,7 +109,7 @@ export default class requestController {
passportName: passportName.toLowerCase().trim(),
role: role.toLowerCase().trim()
}],
status: 'open',
status: 'open'
});
return Response.success(res, 201, res.__('Request created successfully'), newRequest);
} catch (err) {
Expand All @@ -129,7 +131,7 @@ export default class requestController {
gender, role, passportName
} = req.body;
const existingRequest = await db.Request.findOne({ where: { id } });
if (!existingRequest || existingRequest.status === 'Approved' || existingRequest.status === 'Rejected') {
if (!existingRequest || existingRequest.status === 'approved' || existingRequest.status === 'rejected') {
return Response.errorResponse(res, 404, res.__('The request does not exist or it\'s either been approved or rejected'));
}
if (existingRequest.email !== user.email || null) {
Expand Down Expand Up @@ -308,4 +310,37 @@ export default class requestController {
return Response.errorResponse(res, 500, err.message);
}
}

/**
* @description request feature
* @static
* @param {Object} req
* @param {Object} res
* @returns {Object} array of request
* @memberof requestController
*/
static async requestSearch(req, res) {
try {
const { user } = req;
const {
id, location, destination, status, reason, departureDate, returnDate
} = req.query;
const field = {
id,
location,
destination,
status,
reason,
departureDate,
returnDate
};
const requests = await TripsService.searchRequest(field, user);
if (requests === 'request not found') {
return Response.errorResponse(res, 404, res.__(requests));
}
return Response.success(res, 200, res.__('requests found'), requests);
} catch (err) {
return Response.errorResponse(res, 500, res.__('server error'));
}
}
}
5 changes: 3 additions & 2 deletions src/middlewares/protectRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export default class protectRoutes {
if (user.role !== 'manager') {
return Response.errorResponse(res, 401, res.__('you are not authorised for this operation'));
}

next();
}

Expand All @@ -93,7 +92,9 @@ export default class protectRoutes {
*/
static checkUserManager(req, res, next) {
const { user } = req;
if (user.managerId === null) Response.errorResponse(res, 401, res.__('user should have manager before performing this operation'));
if (user.managerId === null) {
return Response.errorResponse(res, 401, res.__('user should have manager before performing this operation'));
}
next();
}

Expand Down
9 changes: 8 additions & 1 deletion src/routes/tripsRoutes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@

import express from 'express';
import tripsController from '../controllers/tripsController';
import { requestRules, returnTripRules, multiCityTripRules } from '../validation/validationRules';
import {
requestRules,
returnTripRules,
multiCityTripRules,
searchQueryRules
} from '../validation/validationRules';
import validationResult from '../validation/validationResult';
import protectRoute from '../middlewares/protectRoute';
import rememberProfile from '../utils/rememberProfile';
Expand All @@ -15,4 +20,6 @@ router.patch('/edit', protectRoute.verifyUser, protectRoute.verifyRequester, rem
router.get('/view', protectRoute.verifyUser, protectRoute.verifyManager, tripsController.availTripRequests);
router.put('/:requestId/confirm', protectRoute.verifyUser, protectRoute.verifyManager, tripsController.confirmRequest);
router.patch('/:requestId/reject', protectRoute.verifyUser, protectRoute.verifyManager, tripsController.rejectRequest);
router.get('/search', protectRoute.verifyUser, searchQueryRules, validationResult, tripsController.requestSearch);

export default router;
48 changes: 31 additions & 17 deletions src/seeders/20200222234112-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module.exports = {
'Requests', [
{
id: '51e74db7-5510-4f50-9f15-e23710331ld5',
location: 'Nairobi',
destination: 'Kigali',
location: 'nairobi',
destination: 'kigali',
reason: 'meeting with partners',
accomodation: 'Virunga-12',
accomodation: 'virunga-12',
departureDate: '2020-01-01',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
email: 'jeanne@andela.com',
Expand All @@ -18,28 +18,28 @@ module.exports = {
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld5',
location: 'Boston',
destination: 'Kigali',
location: 'boston',
destination: 'kigali',
reason: 'meeting with engineers',
accomodation: 'Marriot',
accomodation: 'marriot',
departureDate: '2020-12-01',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
email: 'jdev@andela.com',
status: 'Rejected',
status: 'rejected',
returnDate: '2021-02-15',
managerId: '75c34027-a2f0-4b50-808e-0c0169fb074w',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld4',
location: 'Boston',
destination: 'Gisenyi',
location: 'boston',
destination: 'gisenyi',
reason: 'meeting with engineers write',
accomodation: 'Marriot',
departureDate: '2020-12-01',
email: 'jdev@andela.com',
status: 'Rejected',
status: 'rejected',
returnDate: '2021-02-15',
confirm: true,
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
Expand All @@ -48,31 +48,45 @@ module.exports = {
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld9',
location: 'Boston',
destination: 'Gisenyi',
location: 'boston',
destination: 'gisenyi',
reason: 'meeting with engineers write',
accomodation: 'Marriot',
accomodation: 'marriot',
departureDate: '2020-12-01',
email: 'jdev@andela.com',
status: 'Rejected',
status: 'rejected',
returnDate: '2021-02-15',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '0ad0ef9d-a926-4c5c-86e6-4d1e22e9ab88',
location: 'Boston',
destination: 'Gisenyi',
location: 'boston',
destination: 'bisenyi',
reason: 'meeting with engineers write',
accomodation: 'Marriot',
accomodation: 'marriot',
departureDate: '2020-12-01',
email: 'rejectuser@andela.com',
status: 'open',
returnDate: '2021-02-15',
managerId: '79660e6f-4b7d-4d21-81ad-74f64e9e1c8a',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: 't1e74db7-h610-4f50-9f45-e2371j331ld7',
location: 'koston',
destination: 'kigali',
reason: 'meeting with engineers',
accomodation: 'marriot',
departureDate: '2021-12-01',
managerId: '0119b84a-99a4-41c0-8a0e-6e0b6c385165',
email: 'jdev@andela.com',
status: 'approved',
returnDate: '2022-02-15',
createdAt: new Date(),
updatedAt: new Date(),
}
],
{},
Expand Down
11 changes: 9 additions & 2 deletions src/services/localesServices/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"User is successfully logged in": "User is successfully logged in",
"Incorrect email or password": "Incorrect email or password",
"User is successfully logged out": "User is successfully logged out",
"No trip requests available": "No trip requests available",
"you are not authorised for this operation": "you are not authorised for this operation",
"Request updated successfully": "Request updated successfully",
"The request does not exist or it's either been approved or rejected": "The request does not exist or it's either been approved or rejected",
Expand Down Expand Up @@ -75,5 +74,13 @@
"user has liked facility": "user has liked facility",
"user has already liked facility": "user has already liked facility",
"user has unliked facility": "user has unliked facility",
"user has already unliked facility": "user has already unliked facility"
"user has already unliked facility": "user has already unliked facility",
"status can only be open, rejected or approved": "status can only be open, rejected or approved",
"request id must be a valid uuid": "request id must be a valid uuid",
"requests found": "requests found",
"No trip requests available": "No trip requests available",
"reason must be atleast one character": "reason must be atleast one character",
"request ID can't be empty, please check your input and enter a valid ID": "request ID can't be empty, please check your input and enter a valid ID",
"destination should only contain letter": "destination should only contain letter",
"Gender must either be Male or Female": "Gender must either be Male or Female"
}
7 changes: 6 additions & 1 deletion src/services/localesServices/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,10 @@
"user has liked facility":"l'utilisateur a aimé l'établissement",
"user has already liked facility": "l'utilisateur a déjà aimé l'établissement",
"user has unliked facility": "l'utilisateur n'a pas aimé l'établissement",
"user has already unliked facility": "l'utilisateur n'a pas aimé l'installation"
"user has already unliked facility": "l'utilisateur n'a pas aimé l'installation",
"requests found": "demandes trouvées",
"request ID can't be empty, please check your input and enter a valid ID": "l'ID de la demande ne peut pas être vide, veuillez vérifier votre saisie et saisir un ID valide",
"status can only be open, rejected or approved": "le statut ne peut être ouvert, rejeté ou approuvé",
"reason must be atleast one character": "la raison doit être au moins un caractère",
"destination should only contain letter": "la destination ne doit contenir qu'une lettre"
}
40 changes: 40 additions & 0 deletions src/services/tripServices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Sequelize } from 'sequelize';
import db from '../models';

const { Op } = Sequelize;
/**
* @class TripsService
* @description this class handles trips services
*/
export default class TripsService {
/**
* set the user token and send the email(servicee)
* @param {object} obj
* @param {object} user
* @returns {object} request array
*/
static async searchRequest(obj, user) {
const searchObj = {};
searchObj.where = {};
let field = { ...obj };
field = JSON.parse(JSON.stringify(field));
const fieldArr = Object.keys(field);
if (user.role === 'requester') {
searchObj.where.email = user.email;
}
if (user.role === 'manager') {
searchObj.where.managerId = user.id;
}
fieldArr.forEach((el) => {
searchObj.where[el] = obj[el];
if (el === 'location' || el === 'departure' || el === 'status' || el === 'accommodation' || el === 'reason' || el === 'destination') {
searchObj.where[el] = { [Op.iLike]: `%${field[el].trim()}%` };
}
});
const requests = await db.Request.findAll(searchObj);
if (requests.length < 1) {
return 'request not found';
}
return requests;
}
}
36 changes: 36 additions & 0 deletions src/swagger/trips.swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,39 @@
* '401':
* description: This request is not yours it is for another manager
* */
/**
* @swagger
* /api/v1/trips/search:
* get:
* tags:
* - Trips
* name: search a trip request
* summary: user can search for a trip request using different request attribute
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: token
* in: header
* description: jwt token of the user
* - name: id
* in: query
* - name: location
* in: query
* - name: destination
* in: query
* - name: departureDate
* in: query
* - name: returnDate
* in: query
* - name: reason
* in: query
* - name: status
* in: query
* responses:
* '200':
* description: request found.
* '404':
* description: request not found.
* */
7 changes: 5 additions & 2 deletions src/tests/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ const {
} = chai;
chai.use(chaiHttp);
describe('Signup Tests', () => {
before(() => {
sinon.stub(sgMail, 'send').returns({
beforeEach(() => {
sinon.stub(sgMail, 'send').resolves({
to: 'aime@amgil.com',
from: 'devrepublic.team@gmail.com',
subject: 'barefoot nomad',
html: 'this is stubbing message'
});
});
afterEach(() => {
sinon.restore();
});
it('should return account created sucessfully.', (done) => {
chai.request(index)
.post('/api/v1/auth/register')
Expand Down
Loading

0 comments on commit 9c302c2

Please sign in to comment.