Skip to content

Commit

Permalink
feat(event): search and filtering for all listings. Fixes MEMB-469
Browse files Browse the repository at this point in the history
  • Loading branch information
serge1peshcoff committed Mar 6, 2019
1 parent 5375353 commit a1b65c7
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 95 deletions.
4 changes: 2 additions & 2 deletions config/index.js.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const deepAssign = require('deep-assign');
const merge = require('../lib/merge');

const config = {
default: {
Expand Down Expand Up @@ -57,7 +57,7 @@ let appConfig = config.default || {};

// If we have the environment config, overwrite the config's fields with its fields
if (config[env]) {
appConfig = deepAssign(appConfig, config[env]);
appConfig = merge(appConfig, config[env]);
}

module.exports = appConfig;
10 changes: 0 additions & 10 deletions lib/applications.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@ const errors = require('./errors');
const { Event, Application } = require('../models');
const helpers = require('./helpers');

exports.listUserAppliedEvents = async (req, res) => {
const applications = await Application.findAll({ where: { user_id: req.user.id }, include: [Event] });
const events = applications.map(a => a.event);

return res.json({
success: true,
data: events,
});
};

exports.listAllApplications = async (req, res) => {
if (!req.permissions.list_applications) {
return errors.makeForbiddenError(res, 'You cannot see applications for this event.');
Expand Down
110 changes: 62 additions & 48 deletions lib/events.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,31 @@
const errors = require('./errors');
const merge = require('./merge');
const helpers = require('./helpers');
const { Event, Application } = require('../models');
const { Sequelize } = require('./sequelize');

/** Requests for all events **/

exports.listEvents = async (req, res) => {
const filter = {
deleted: false, // Filter out deleted events,
status: 'published'
};
// Get default query obj.
const defaultQueryObj = helpers.getDefaultQuery(req);

if (req.query.type) {
filter.type = Array.isArray(req.query.type) ? { [Sequelize.Op.in]: req.query.type } : req.query.type;
}

if (req.query.displayPast === false) {
filter.starts = { [Sequelize.Op.gte]: new Date() };
}

if (req.query.search) {
filter[Sequelize.Op.or] = [
{ name: { [Sequelize.Op.iLike]: '%' + req.query.search + '%' } },
{ description: { [Sequelize.Op.iLike]: '%' + req.query.search + '%' } }
];
}

const events = await Event.findAll({ where: filter });

let queryOffset = 0;
let queryLimit = events.length;

if (req.query.offset) {
const offset = parseInt(req.query.offset, 10);
if (!Number.isNaN(offset) && offset >= 0) {
queryOffset = offset;
}
}

if (req.query.limit) {
const limit = parseInt(req.query.limit, 10);
if (!Number.isNaN(limit) && limit > 0) {
queryLimit = limit;
// Applying custom filter: deleted = false, status === published.
const queryObj = merge(defaultQueryObj, {
where: {
deleted: false,
status: 'published'
}
}
});

const eventsWithOffsetAndLimit = events.slice(queryOffset, queryOffset + queryLimit);
const events = await Event.findAll(queryObj);

return res.json({
success: true,
data: eventsWithOffsetAndLimit,
data: events,
meta: {
offset: queryOffset,
limit: queryLimit,
moreAvailable: (queryOffset + queryLimit) < events.length
offset: queryObj.offset,
limit: queryObj.limit
}
});
};
Expand Down Expand Up @@ -83,13 +55,55 @@ exports.listBodyApplications = async (req, res) => {

// Returns all events the user is organizer on
exports.listUserOrganizedEvents = async (req, res) => {
const events = await Event.findAll({ where: { deleted: false } });
const defaultQueryObj = helpers.getDefaultQuery(req);
const queryObj = merge(defaultQueryObj, {
where: {
deleted: false,
organizers: { [Sequelize.Op.contains] : [{ user_id: req.user.id }] }
}
});

const filteredEvents = events.filter(event => event.organizers.some(org => org.user_id === req.user.id));
const queryObj2 = {
where: {
deleted: false,
organizers: { [Sequelize.Op.contains] : [{ user_id: req.user.id }] }
}
}

const events = await Event.findAll(queryObj2);

return res.json({
success: true,
data: filteredEvents,
data: events,
});
};

// List all the event where the user is participant at.
exports.listUserAppliedEvents = async (req, res) => {
const defaultQueryObj = helpers.getDefaultQuery(req);
const queryObj = merge(defaultQueryObj, {
where: {
deleted: false,
'$applications.user_id$': req.user.id
},
subQuery: false,
include: [{
model: Application,
attributes: ['user_id'], // we only need user_id here
required: true
}]
});

// The subQuery: false line is super important as if we'll remove it,
// the query fwill fail with `missing FROM-clause entry for table "applications"`
// error. It's a regression bug in Sequelize, more info
// here: https://github.com/sequelize/sequelize/issues/9869

const events = await Event.findAll(queryObj);

return res.json({
success: true,
data: events,
});
};

Expand Down Expand Up @@ -161,15 +175,15 @@ exports.eventDetails = async (req, res) => {
};

exports.editEvent = async (req, res) => {
// If user can't edit anything, return error right away
// If user can't edit anything, return error right away
if (!req.permissions.edit_event) {
return errors.makeForbiddenError(res, 'You cannot edit this event');
}

const data = req.body;
const event = req.event;

// Disallow changing applications and organizers, use separate requests for that
// Disallow changing applications and organizers, use separate requests for that
delete data.applications;
delete data.organizing_locals;
delete data.organizers;
Expand All @@ -196,7 +210,7 @@ exports.deleteEvent = async (req, res) => {
return errors.makeForbiddenError(res, 'You are not permitted to delete this event.');
}

// Deletion is only setting the 'deleted' field to true.
// Deletion is only setting the 'deleted' field to true.
await req.event.update({ deleted: true });

return res.json({
Expand All @@ -206,7 +220,7 @@ exports.deleteEvent = async (req, res) => {
};

exports.setApprovalStatus = async (req, res) => {
// If there is no transition found, it's disallowed to everybody.
// If there is no transition found, it's disallowed to everybody.
if (!req.permissions.set_status) {
return errors.makeForbiddenError(res, 'You are not allowed to change status.');
}
Expand Down
45 changes: 45 additions & 0 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
const moment = require('moment');

const constants = require('./constants');
const { Sequelize } = require('./sequelize');

// A helper to get default search/query/pagination filter for events listings.
exports.getDefaultQuery = (req) => {
// Default filter is empty.
const queryObj = {
where: {}
}

// If search is set, searching for event by name or description case-insensitive.
if (req.query.search) {
queryObj.where[Sequelize.Op.or] = [
{ name: { [Sequelize.Op.iLike]: '%' + req.query.search + '%' } },
{ description: { [Sequelize.Op.iLike]: '%' + req.query.search + '%' } }
];
}

// If event type is set, filter on it.
if (req.query.type) {
queryObj.where.type = Array.isArray(req.query.type) ? { [Sequelize.Op.in]: req.query.type } : req.query.type;
}

// If displayPast === false, only displaying future events.
if (req.query.displayPast === false) {
queryObj.where.starts = { [Sequelize.Op.gte]: new Date() };
}

// If offset is set and is valid, use it.
if (req.query.offset) {
const offset = parseInt(req.query.offset, 10);
if (!Number.isNaN(offset) && offset >= 0) {
queryObj.offset = offset;
}
}

// If limit is set and is valid, use it.
if (req.query.limit) {
const limit = parseInt(req.query.limit, 10);
if (!Number.isNaN(limit) && limit > 0) {
queryObj.limit = limit;
}
}

return queryObj;
};

// A helper to flatten the nested object. Copypasted from Google.
exports.flattenObject = (obj, prefix = '') => {
Expand Down
26 changes: 26 additions & 0 deletions lib/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// A helper to deep-merge 2 objects.
// This is here and not in helpers.js to avoid circular modules
// loading breaking stuff.
const mergeDeep = (target, source) => {
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item);

const keys = [
...Object.keys(source),
...Object.getOwnPropertySymbols(source)
];

for (const key of keys) {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
mergeDeep(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}

return target
};

module.exports = mergeDeep;
2 changes: 1 addition & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ GeneralRouter.get('/', events.listEvents);
GeneralRouter.post('/', events.addEvent);

GeneralRouter.get('/mine/organizing', events.listUserOrganizedEvents);
GeneralRouter.get('/mine/participating', applications.listUserAppliedEvents);
GeneralRouter.get('/mine/participating', events.listUserAppliedEvents);
GeneralRouter.get('/mine/approvable', events.listApprovableEvents);
GeneralRouter.get('/boardview/:body_id', events.listBodyApplications);

Expand Down
43 changes: 18 additions & 25 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a1b65c7

Please sign in to comment.