From c83d42f3e79145f5d4a46f596a66a5b487419985 Mon Sep 17 00:00:00 2001 From: Agata-Andrzejewska Date: Sat, 31 Mar 2018 18:02:33 +0200 Subject: [PATCH] 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) --- apidoc.raml | 1 + controllers/goto/ideas.js | 3 +- controllers/ideas.js | 25 ++++- controllers/validators/ideas.js | 1 + controllers/validators/parser.js | 3 +- controllers/validators/schema/ideas.js | 24 +++- models/idea/index.js | 31 ++++++ routes/ideas.js | 5 + serializers/ideas.js | 4 +- test/ideas.list.js | 146 +++++++++++++++++++++++++ 10 files changed, 238 insertions(+), 5 deletions(-) diff --git a/apidoc.raml b/apidoc.raml index a6e061f..33f7e9d 100644 --- a/apidoc.raml +++ b/apidoc.raml @@ -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. diff --git a/controllers/goto/ideas.js b/controllers/goto/ideas.js index e817a30..8910f34 100644 --- a/controllers/goto/ideas.js +++ b/controllers/goto/ideas.js @@ -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']) }, }; diff --git a/controllers/ideas.js b/controllers/ideas.js index dc5e3bb..d45a495 100644 --- a/controllers/ideas.js +++ b/controllers/ideas.js @@ -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 }; diff --git a/controllers/validators/ideas.js b/controllers/validators/ideas.js index b4aa5f9..c721d84 100644 --- a/controllers/validators/ideas.js +++ b/controllers/validators/ideas.js @@ -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'), diff --git a/controllers/validators/parser.js b/controllers/validators/parser.js index c1a3057..057e53f 100644 --- a/controllers/validators/parser.js +++ b/controllers/validators/parser.js @@ -27,7 +27,8 @@ const parametersDictionary = { relatedToTags: 'array', size: 'int', creators: 'array', - commentedBy: 'array' + commentedBy: 'array', + highlyVoted: 'int' }, }; diff --git a/controllers/validators/schema/ideas.js b/controllers/validators/schema/ideas.js index 010232f..077e690 100644 --- a/controllers/validators/schema/ideas.js +++ b/controllers/validators/schema/ideas.js @@ -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 }; diff --git a/models/idea/index.js b/models/idea/index.js index a9fbed6..66615ce 100644 --- a/models/idea/index.js +++ b/models/idea/index.js @@ -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} - 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; diff --git a/routes/ideas.js b/routes/ideas.js index 43ed8df..4c39f05 100644 --- a/routes/ideas.js +++ b/routes/ideas.js @@ -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 diff --git a/serializers/ideas.js b/serializers/ideas.js index 6b0ed23..80fefe5 100644 --- a/serializers/ideas.js +++ b/serializers/ideas.js @@ -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) { diff --git a/test/ideas.list.js b/test/ideas.list.js index 3bd7700..13ab1f0 100644 --- a/test/ideas.list.js +++ b/test/ideas.list.js @@ -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); + }); + }); + }); });