Skip to content

Commit c83d42f

Browse files
committed
GET /ideas?filter[highlyRated]=bottomRateLimit
* create GET /ideas?filter[highlyRated]=bottomRateLimit * renaming rate => voteSum, putting voteSum to meta in response * rename + fix in query (we were ignoring ideas with 0 votes before)
1 parent 3273c00 commit c83d42f

File tree

10 files changed

+238
-5
lines changed

10 files changed

+238
-5
lines changed

apidoc.raml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ types:
373373
- new ideas: `?sort=-created`
374374
- ideas with provided creators: `?filter[creators]=username0,username1,username2`
375375
- ideas commented by provided users: `?filter[commentedBy]=username0,username1,username2`
376+
- highly voted ideas with minimum amount of votes parameter: `?filter[highlyVoted]=bottomValueLimit`
376377
/{id}:
377378
get:
378379
description: Read an idea by id.

controllers/goto/ideas.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99
new: route(['query.sort'], 'newQuery'),
1010
random: route(['query.filter.random']),
1111
withCreators: route(['query.filter.creators']),
12-
commentedBy: route(['query.filter.commentedBy'])
12+
commentedBy: route(['query.filter.commentedBy']),
13+
highlyVoted: route(['query.filter.highlyVoted'])
1314
},
1415
};

controllers/ideas.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,5 +231,28 @@ async function getIdeasCommentedBy(req, res, next) {
231231
}
232232
}
233233

234+
/**
235+
* Get highly voted ideas with an optional parameter of minimum votes
236+
*/
237+
async function getIdeasHighlyVoted(req, res, next) {
238+
try {
239+
// gather data
240+
const { page: { offset = 0, limit = 5 } = { } } = req.query;
241+
const { highlyVoted } = req.query.filter;
242+
243+
// read ideas from database
244+
const foundIdeas = await models.idea.findHighlyVoted(highlyVoted, { offset, limit });
245+
246+
// serialize
247+
const serializedIdeas = serialize.idea(foundIdeas);
248+
249+
// respond
250+
return res.status(200).json(serializedIdeas);
251+
252+
} catch (e) {
253+
return next(e);
254+
}
255+
}
256+
234257

235-
module.exports = { get, getIdeasCommentedBy, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };
258+
module.exports = { get, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patch, post };

controllers/validators/ideas.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const validate = require('./validate-by-schema');
55
module.exports = {
66
get: validate('getIdea'),
77
getIdeasCommentedBy: validate('getIdeasCommentedBy'),
8+
getIdeasHighlyVoted: validate('getIdeasHighlyVoted'),
89
getIdeasWithCreators: validate('getIdeasWithCreators'),
910
getIdeasWithMyTags: validate('getIdeasWithMyTags'),
1011
getIdeasWithTags: validate('getIdeasWithTags'),

controllers/validators/parser.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const parametersDictionary = {
2727
relatedToTags: 'array',
2828
size: 'int',
2929
creators: 'array',
30-
commentedBy: 'array'
30+
commentedBy: 'array',
31+
highlyVoted: 'int'
3132
},
3233
};
3334

controllers/validators/schema/ideas.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,26 @@ const getIdeasCommentedBy = {
173173
required: ['query']
174174
};
175175

176-
module.exports = { getIdea, getIdeasCommentedBy, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };
176+
const getIdeasHighlyVoted = {
177+
properties: {
178+
query: {
179+
properties: {
180+
filter: {
181+
properties: {
182+
highlyVoted: {
183+
type: 'number'
184+
}
185+
},
186+
required: ['highlyVoted'],
187+
additionalProperties: false
188+
},
189+
page
190+
},
191+
required: ['filter'],
192+
additionalProperties: false
193+
},
194+
},
195+
required: ['query']
196+
};
197+
198+
module.exports = { getIdea, getIdeasCommentedBy, getIdeasHighlyVoted, getIdeasWithCreators, getIdeasWithMyTags, getIdeasWithTags, getNewIdeas, getRandomIdeas, patchIdea, postIdeas };

models/idea/index.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,37 @@ class Idea extends Model {
274274
const cursor = await this.db.query(query, params);
275275
return await cursor.all();
276276
}
277+
278+
279+
/**
280+
* Read ideas commented by specified users
281+
* @param {string[]} voteSumBottomLimit - minimal query voteSum
282+
* @param {integer} offset - pagination offset
283+
* @param {integer} limit - pagination limit
284+
* @returns {Promise<Idea[]>} - list of found ideas
285+
*/
286+
static async findHighlyVoted(voteSumBottomLimit, { offset, limit }) {
287+
const query = `
288+
FOR idea IN ideas
289+
LET ideaVotes = (FOR vote IN votes FILTER idea._id == vote._to RETURN vote)
290+
// get sum of each idea's votes values
291+
LET voteSum = SUM(ideaVotes[*].value)
292+
// set bottom limit of voteSum
293+
FILTER voteSum >= @voteSumBottomLimit
294+
// find creator
295+
LET c = (DOCUMENT(idea.creator))
296+
LET creator = MERGE(KEEP(c, 'username'), c.profile)
297+
LET ideaOut = MERGE(KEEP(idea, 'title', 'detail', 'created'), { id: idea._key}, { creator }, { voteSum })
298+
299+
// sort by amount of votes
300+
SORT ideaOut.voteSum DESC, ideaOut.created DESC
301+
LIMIT @offset, @limit
302+
RETURN ideaOut`;
303+
304+
const params = { voteSumBottomLimit, offset, limit };
305+
const cursor = await this.db.query(query, params);
306+
return await cursor.all();
307+
}
277308
}
278309

279310
module.exports = Idea;

routes/ideas.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ router.route('/')
4040
router.route('/')
4141
.get(go.get.commentedBy, authorize.onlyLogged, parse, ideaValidators.getIdeasCommentedBy, ideaControllers.getIdeasCommentedBy);
4242

43+
// get ideas commented by specified users
44+
router.route('/')
45+
.get(go.get.highlyVoted, authorize.onlyLogged, parse, ideaValidators.getIdeasHighlyVoted, ideaControllers.getIdeasHighlyVoted);
46+
47+
4348

4449
router.route('/:id')
4550
// read idea by id

serializers/ideas.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ const ideaSerializer = new Serializer('ideas', {
5757
myVote(record, current) {
5858
if (!current.hasOwnProperty('myVote')) return;
5959
return (current.myVote) ? current.myVote.value : 0;
60+
},
61+
voteSum(record, current) {
62+
return current.voteSum;
6063
}
6164
}
62-
6365
});
6466

6567
function idea(data) {

test/ideas.list.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,4 +684,150 @@ describe('read lists of ideas', () => {
684684
});
685685
});
686686
});
687+
688+
describe('GET /ideas?filter[highlyVoted]', () => {
689+
let user0;
690+
// create and save testing data
691+
beforeEach(async () => {
692+
const primarys = 'ideas';
693+
const data = {
694+
users: 6,
695+
tags: 6,
696+
verifiedUsers: [0, 1, 2, 3, 4],
697+
ideas: [[{}, 0], [{}, 0],[{}, 1],[{}, 2],[{}, 2],[{}, 2],[{}, 3]],
698+
userTag: [
699+
[0,0,'',5],[0,1,'',4],[0,2,'',3],[0,4,'',1],
700+
[1,1,'',4],[1,3,'',2],
701+
[2,5,'',2]
702+
],
703+
ideaTags: [
704+
[0,0],[0,1],[0,2],
705+
[1,1],[1,2],
706+
[2,1],[2,2],[2,4],
707+
[4,0],[4,1],[4,2],[4,3],[4,4],
708+
[5,2],[5,3],
709+
[6,3]
710+
],
711+
// odeas with votes: 3:3, 1:3, 5:1, 2:1, 0:0, 6: -1, 4:-2
712+
votes: [
713+
[0, [primarys, 0], -1],
714+
[1, [primarys, 0], 1],
715+
[0, [primarys, 1], 1],
716+
[1, [primarys, 1], 1],
717+
[2, [primarys, 1], 1],
718+
[0, [primarys, 2], -1],
719+
[1, [primarys, 2], 1],
720+
[2, [primarys, 2], 1],
721+
[0, [primarys, 3], 1],
722+
[1, [primarys, 3], 1],
723+
[2, [primarys, 3], 1],
724+
[3, [primarys, 3], 1],
725+
[4, [primarys, 3], -1],
726+
[0, [primarys, 4], -1],
727+
[1, [primarys, 4], -1],
728+
[3, [primarys, 5], 1],
729+
[3, [primarys, 6], -1]
730+
]
731+
};
732+
733+
dbData = await dbHandle.fill(data);
734+
735+
[user0, , , , , ] = dbData.users;
736+
});
737+
738+
context('logged in', () => {
739+
740+
beforeEach(() => {
741+
agent = agentFactory.logged(user0);
742+
});
743+
744+
context('valid data', () => {
745+
746+
it('[highly voted ideas] 200 and return array of matched ideas', async () => {
747+
748+
// request
749+
const response = await agent
750+
.get('/ideas?filter[highlyVoted]=0')
751+
.expect(200);
752+
753+
// without pagination, limit for ideas 5 we should find 5 ideas...
754+
should(response.body).have.property('data').Array().length(5);
755+
756+
// sorted by creation date desc
757+
should(response.body.data.map(idea => idea.attributes.title))
758+
.eql([3, 1, 5, 2, 0].map(no => `idea title ${no}`));
759+
760+
});
761+
762+
it('[highly voted ideas with at least 2 votes in plus] 200 and return array of matched ideas', async () => {
763+
764+
// request
765+
const response = await agent
766+
.get('/ideas?filter[highlyVoted]=2')
767+
.expect(200);
768+
769+
// without pagination, limit for ideas 5 we should find 5 ideas...
770+
should(response.body).have.property('data').Array().length(2);
771+
772+
// sorted by creation date desc
773+
should(response.body.data.map(idea => idea.attributes.title))
774+
.eql([3, 1].map(no => `idea title ${no}`));
775+
776+
// shoud value be at least 2
777+
should(Math.min(...response.body.data.map(idea => idea.meta.voteSum)))
778+
.aboveOrEqual(2);
779+
});
780+
781+
782+
it('[pagination] offset and limit the results', async () => {
783+
const response = await agent
784+
.get('/ideas?filter[highlyVoted]=0&page[offset]=1&page[limit]=3')
785+
.expect(200);
786+
787+
// we should find 3 ideas
788+
should(response.body).have.property('data').Array().length(3);
789+
790+
// sorted by creation date desc
791+
should(response.body.data.map(idea => idea.attributes.title))
792+
.eql([1, 5, 2].map(no => `idea title ${no}`));
793+
});
794+
795+
});
796+
797+
context('invalid data', () => {
798+
799+
it('[invalid query.filter.highlyVoted] 400', async () => {
800+
await agent
801+
.get('/ideas?filter[highlyVoted]=string')
802+
.expect(400);
803+
});
804+
805+
it('[invalid query.filter.highlyVoted] 400', async () => {
806+
await agent
807+
.get('/ideas?filter[highlyVoted]')
808+
.expect(400);
809+
});
810+
811+
it('[invalid pagination] 400', async () => {
812+
await agent
813+
.get('/ideas?filter[highlyVoted]=0&page[offset]=1&page[limit]=21')
814+
.expect(400);
815+
});
816+
817+
it('[unexpected query params] 400', async () => {
818+
await agent
819+
.get('/ideas?filter[highlyVoted]=0&additional[param]=3&page[offset]=1&page[limit]=3')
820+
.expect(400);
821+
});
822+
});
823+
});
824+
825+
context('not logged in', () => {
826+
it('403', async () => {
827+
await agent
828+
.get('/ideas?filter[highlyVoted]=0')
829+
.expect(403);
830+
});
831+
});
832+
});
687833
});

0 commit comments

Comments
 (0)