Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create GET /ideas?filter[highlyRated]=bottomRateLimit #56

Merged
merged 1 commit into from
Apr 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apidoc.raml
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ types:
- new ideas: `?sort=-created`
- ideas with provided creators: `?filter[creators]=username0,username1,username2`
- ideas commented by provided users: `?filter[commentedBy]=username0,username1,username2`
- highly voted ideas with minimum amount of votes parameter: `?filter[highlyVoted]=bottomValueLimit`
/{id}:
get:
description: Read an idea by id.
Expand Down
3 changes: 2 additions & 1 deletion controllers/goto/ideas.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
new: route(['query.sort'], 'newQuery'),
random: route(['query.filter.random']),
withCreators: route(['query.filter.creators']),
commentedBy: route(['query.filter.commentedBy'])
commentedBy: route(['query.filter.commentedBy']),
highlyVoted: route(['query.filter.highlyVoted'])
},
};
25 changes: 24 additions & 1 deletion controllers/ideas.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,28 @@ async function getIdeasCommentedBy(req, res, next) {
}
}

/**
* Get highly voted ideas with an optional parameter of minimum votes
*/
async function getIdeasHighlyVoted(req, res, next) {
try {
// gather data
const { page: { offset = 0, limit = 5 } = { } } = req.query;
const { highlyVoted } = req.query.filter;

// read ideas from database
const foundIdeas = await models.idea.findHighlyVoted(highlyVoted, { offset, limit });

// serialize
const serializedIdeas = serialize.idea(foundIdeas);

// respond
return res.status(200).json(serializedIdeas);

} catch (e) {
return next(e);
}
}


module.exports = { get, getIdeasCommentedBy, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };
module.exports = { get, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };
1 change: 1 addition & 0 deletions controllers/validators/ideas.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const validate = require('./validate-by-schema');
module.exports = {
get: validate('getIdea'),
getIdeasCommentedBy: validate('getIdeasCommentedBy'),
getIdeasHighlyVoted: validate('getIdeasHighlyVoted'),
getIdeasWithCreators: validate('getIdeasWithCreators'),
getIdeasWithMyTags: validate('getIdeasWithMyTags'),
getIdeasWithTags: validate('getIdeasWithTags'),
Expand Down
3 changes: 2 additions & 1 deletion controllers/validators/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const parametersDictionary = {
relatedToTags: 'array',
size: 'int',
creators: 'array',
commentedBy: 'array'
commentedBy: 'array',
highlyVoted: 'int'
},
};

Expand Down
24 changes: 23 additions & 1 deletion controllers/validators/schema/ideas.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,26 @@ const getIdeasCommentedBy = {
required: ['query']
};

module.exports = { getIdea, getIdeasCommentedBy, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };
const getIdeasHighlyVoted = {
properties: {
query: {
properties: {
filter: {
properties: {
highlyVoted: {
type: 'number'
}
},
required: ['highlyVoted'],
additionalProperties: false
},
page
},
required: ['filter'],
additionalProperties: false
},
},
required: ['query']
};

module.exports = { getIdea, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };
31 changes: 31 additions & 0 deletions models/idea/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,37 @@ class Idea extends Model {
const cursor = await this.db.query(query, params);
return await cursor.all();
}


/**
* Read ideas commented by specified users
* @param {string[]} voteSumBottomLimit - minimal query voteSum
* @param {integer} offset - pagination offset
* @param {integer} limit - pagination limit
* @returns {Promise<Idea[]>} - list of found ideas
*/
static async findHighlyVoted(voteSumBottomLimit, { offset, limit }) {
const query = `
FOR idea IN ideas
LET ideaVotes = (FOR vote IN votes FILTER idea._id == vote._to RETURN vote)
// get sum of each idea's votes values
LET voteSum = SUM(ideaVotes[*].value)
// set bottom limit of voteSum
FILTER voteSum >= @voteSumBottomLimit
// find creator
LET c = (DOCUMENT(idea.creator))
LET creator = MERGE(KEEP(c, 'username'), c.profile)
LET ideaOut = MERGE(KEEP(idea, 'title', 'detail', 'created'), { id: idea._key}, { creator }, { voteSum })

// sort by amount of votes
SORT ideaOut.voteSum DESC, ideaOut.created DESC
LIMIT @offset, @limit
RETURN ideaOut`;

const params = { voteSumBottomLimit, offset, limit };
const cursor = await this.db.query(query, params);
return await cursor.all();
}
}

module.exports = Idea;
5 changes: 5 additions & 0 deletions routes/ideas.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ router.route('/')
router.route('/')
.get(go.get.commentedBy, authorize.onlyLogged, parse, ideaValidators.getIdeasCommentedBy, ideaControllers.getIdeasCommentedBy);

// get ideas commented by specified users
router.route('/')
.get(go.get.highlyVoted, authorize.onlyLogged, parse, ideaValidators.getIdeasHighlyVoted, ideaControllers.getIdeasHighlyVoted);



router.route('/:id')
// read idea by id
Expand Down
4 changes: 3 additions & 1 deletion serializers/ideas.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ const ideaSerializer = new Serializer('ideas', {
myVote(record, current) {
if (!current.hasOwnProperty('myVote')) return;
return (current.myVote) ? current.myVote.value : 0;
},
voteSum(record, current) {
return current.voteSum;
}
}

});

function idea(data) {
Expand Down
146 changes: 146 additions & 0 deletions test/ideas.list.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,150 @@ describe('read lists of ideas', () => {
});
});
});

describe('GET /ideas?filter[highlyVoted]', () => {
let user0;
// create and save testing data
beforeEach(async () => {
const primarys = 'ideas';
const data = {
users: 6,
tags: 6,
verifiedUsers: [0, 1, 2, 3, 4],
ideas: [[{}, 0], [{}, 0],[{}, 1],[{}, 2],[{}, 2],[{}, 2],[{}, 3]],
userTag: [
[0,0,'',5],[0,1,'',4],[0,2,'',3],[0,4,'',1],
[1,1,'',4],[1,3,'',2],
[2,5,'',2]
],
ideaTags: [
[0,0],[0,1],[0,2],
[1,1],[1,2],
[2,1],[2,2],[2,4],
[4,0],[4,1],[4,2],[4,3],[4,4],
[5,2],[5,3],
[6,3]
],
// odeas with votes: 3:3, 1:3, 5:1, 2:1, 0:0, 6: -1, 4:-2
votes: [
[0, [primarys, 0], -1],
[1, [primarys, 0], 1],
[0, [primarys, 1], 1],
[1, [primarys, 1], 1],
[2, [primarys, 1], 1],
[0, [primarys, 2], -1],
[1, [primarys, 2], 1],
[2, [primarys, 2], 1],
[0, [primarys, 3], 1],
[1, [primarys, 3], 1],
[2, [primarys, 3], 1],
[3, [primarys, 3], 1],
[4, [primarys, 3], -1],
[0, [primarys, 4], -1],
[1, [primarys, 4], -1],
[3, [primarys, 5], 1],
[3, [primarys, 6], -1]
]
};

dbData = await dbHandle.fill(data);

[user0, , , , , ] = dbData.users;
});

context('logged in', () => {

beforeEach(() => {
agent = agentFactory.logged(user0);
});

context('valid data', () => {

it('[highly voted ideas] 200 and return array of matched ideas', async () => {

// request
const response = await agent
.get('/ideas?filter[highlyVoted]=0')
.expect(200);

// without pagination, limit for ideas 5 we should find 5 ideas...
should(response.body).have.property('data').Array().length(5);

// sorted by creation date desc
should(response.body.data.map(idea => idea.attributes.title))
.eql([3, 1, 5, 2, 0].map(no => `idea title ${no}`));

});

it('[highly voted ideas with at least 2 votes in plus] 200 and return array of matched ideas', async () => {

// request
const response = await agent
.get('/ideas?filter[highlyVoted]=2')
.expect(200);

// without pagination, limit for ideas 5 we should find 5 ideas...
should(response.body).have.property('data').Array().length(2);

// sorted by creation date desc
should(response.body.data.map(idea => idea.attributes.title))
.eql([3, 1].map(no => `idea title ${no}`));

// shoud value be at least 2
should(Math.min(...response.body.data.map(idea => idea.meta.voteSum)))
.aboveOrEqual(2);
});


it('[pagination] offset and limit the results', async () => {
const response = await agent
.get('/ideas?filter[highlyVoted]=0&page[offset]=1&page[limit]=3')
.expect(200);

// we should find 3 ideas
should(response.body).have.property('data').Array().length(3);

// sorted by creation date desc
should(response.body.data.map(idea => idea.attributes.title))
.eql([1, 5, 2].map(no => `idea title ${no}`));
});

});

context('invalid data', () => {

it('[invalid query.filter.highlyVoted] 400', async () => {
await agent
.get('/ideas?filter[highlyVoted]=string')
.expect(400);
});

it('[invalid query.filter.highlyVoted] 400', async () => {
await agent
.get('/ideas?filter[highlyVoted]')
.expect(400);
});

it('[invalid pagination] 400', async () => {
await agent
.get('/ideas?filter[highlyVoted]=0&page[offset]=1&page[limit]=21')
.expect(400);
});

it('[unexpected query params] 400', async () => {
await agent
.get('/ideas?filter[highlyVoted]=0&additional[param]=3&page[offset]=1&page[limit]=3')
.expect(400);
});
});
});

context('not logged in', () => {
it('403', async () => {
await agent
.get('/ideas?filter[highlyVoted]=0')
.expect(403);
});
});
});
});