# HG changeset patch # User Quentin Raynaud <quentin@qraynaud.eu> # Date 1397739181 -7200 # Thu Apr 17 14:53:01 2014 +0200 # Node ID dea87ac0547a87280a36ce1ef34256a75e5a1043 # Parent a6f23bf3d5ead0213ae8839845fefa0c30e06db5 feat(api/talk): add voting ability to talk API diff --git a/server/es-config/create_index.json b/server/es-config/create_index.json --- a/server/es-config/create_index.json +++ b/server/es-config/create_index.json @@ -33,7 +33,7 @@ "index": "no" }, "name" : { - "type" : "string" + "type" : "string" }, "photo" : { "type" : "string", @@ -114,6 +114,21 @@ } } } + }, + "talk_votes": { + "dynamic": "strict", + "_parent": { + "type": "talk" + }, + "properties": { + "member_id": { + "type": "string", + "index": "not_analyzed" + }, + "value": { + "type": "integer" + } + } } } } diff --git a/server/libs/api/esobject.js b/server/libs/api/esobject.js --- a/server/libs/api/esobject.js +++ b/server/libs/api/esobject.js @@ -258,6 +258,8 @@ // Creates a proper deferred object from a list defFromList: function(list) { + if (!list || !list.length) + return deferred(true); // Calls deferred with each item in the list as an argument return deferred.apply(deferred, list); }, @@ -269,12 +271,16 @@ Object.defineProperties(this, { _id: { value: id, - writable: true + writable: true, }, _version: { value: version, - writable: true - } + writable: true, + }, + _parent: { + value: undefined, + writable: true, + }, }); }; @@ -329,6 +335,9 @@ // New class (provide the object's id & version) var obj = new Class(esObj._id, esObj._version); + if (esObj._parent || (esObj.fields && esObj.fields._parent)) + obj._parent = esObj._parent || esObj.fields._parent; + // For each source's attributes, copy it to the new instance _.forOwn(esObj._source, function(val, key) { obj[key] = val; @@ -349,6 +358,9 @@ // Search instances in ES that match the given query and return an // array of instances (with an extra export method added to it) Class.search = function(query) { + query.fields = query.fields || []; + query.fields.push('_id', '_source', '_parent'); + // Do the query return Class.es_search(query) .then(function(res) { @@ -418,6 +430,9 @@ // Adds version to it if there is one if (this._version) config.version = this._version; + // Adds parent to it if there is one + if (this._parent) + config.parent = this._parent; // Put the object into the index return this.constructor.es_index(config, this) @@ -440,7 +455,8 @@ // Creates configuration var config = _.extend({}, this.constructor.config.descr.db, { - _id: this._id + _id: this._id, + parent: this._parent, }); // Adds version to it if there is one if (this._version) diff --git a/server/libs/api/index.js b/server/libs/api/index.js --- a/server/libs/api/index.js +++ b/server/libs/api/index.js @@ -2,3 +2,4 @@ module.exports.User = require('./user'); module.exports.Talk = require('./talk'); +module.exports.TalkVote = require('./talk_vote'); diff --git a/server/libs/api/talk.js b/server/libs/api/talk.js --- a/server/libs/api/talk.js +++ b/server/libs/api/talk.js @@ -3,6 +3,7 @@ var assert = require('assert'); var ESObject = require('./esobject'); var User = require('./user'); +var TalkVote = require('./talk_vote'); var moment = require('moment'); var validator = require('validator'); var config = require('../../config.js').elasticsearch; @@ -148,6 +149,69 @@ name: 'Unkown (error retrieving user)' }; }); + }, + votes: function(ign, ign2, obj) { + return TalkVote.search({ + size: 0, + aggregations: { + positive_votes: { + filter: { + and: [{ + has_parent: { + type: 'talk', + filter: { + term: { + _id: obj._id, + }, + }, + }, + }, { + range: { + value: { + gt: 0, + }, + }, + }], + }, + aggregations: { + total: { + sum: { field: 'value' }, + }, + }, + }, + negative_votes: { + filter: { + and: [{ + has_parent: { + type: 'talk', + filter: { + term: { + _id: obj._id, + }, + }, + }, + }, { + range: { + value: { + lt: 0, + }, + }, + }], + }, + aggregations: { + total: { + sum: { field: 'value' }, + }, + }, + }, + }, + }) + .then(function (res) { + return { + positive: res.aggregations.positive_votes.total.value, + negative: -res.aggregations.negative_votes.total.value, + }; + }); } - } + }, }); diff --git a/server/libs/api/talk_vote.js b/server/libs/api/talk_vote.js new file mode 100644 --- /dev/null +++ b/server/libs/api/talk_vote.js @@ -0,0 +1,33 @@ +'use strict'; + +var assert = require('assert'); +var config = require('../../config.js').elasticsearch; +var _ = require('lodash'); +var validator = require('validator'); + +var ESObject = require('./esobject'); + +function id(oldVal, newVal) { + return oldVal || newVal; +} + +var TalkVote = module.exports = ESObject.create({ + db: _.extend({ + _type: 'talk_votes', + }, config), + import: { + $check: function(oldObj, resObj) { + // Check that value is an int between -1 and 1 + assert(validator.isInt(resObj.value), 'value should be an int'); + assert(-1 <= resObj.value && resObj.value <= 1, 'value should be between -1 and 1'); + }, + value: function (oldVal, newVal) { + return validator.toInt(newVal || oldVal); + }, + member_id: id, + }, + export: { + id: '<%= _id %>', + talk: '<%= _parent %>', + }, +}); diff --git a/server/routes/talk.js b/server/routes/talk.js --- a/server/routes/talk.js +++ b/server/routes/talk.js @@ -2,8 +2,10 @@ var express = require('express'); var Talk = require('../libs/api').Talk; +var TalkVote = require('../libs/api').TalkVote; var moment = require('moment'); var _ = require('lodash'); +var url = require('url'); module.exports = function(app) { function termRequest(field) { @@ -220,6 +222,7 @@ .then(function (talk) { if (req.user.member_id === talk.author || req.user.isAdmin) return talk; + throw _.extend(new Error(), {message: 'You are not the owner of this talk', code: 'not_authorized', name: 'Authorization error'}); }) .then(function (talk) { @@ -281,4 +284,78 @@ }); }); + // Voting APIs + app.get('/api/v1/talk/:id/_vote', function(req, res) { + TalkVote + .search({ + size: 1, + filter: { + and: [{ + term: {member_id: req.user.member_id}, + }, { + has_parent: { + type: 'talk', + filter: { + term: {_id: req.params.id}, + }, + }, + }], + }, + }) + .then(function (res) { + if (!res.total) + return {value: 0}; + return res.elements[0]; + }) + .then(res.json.bind(res, 200)) + .catch(function(err) { + err.code = err.code || 'es-error'; + res.json(500, err); + }); + }); + + app.all('/api/v1/talk/:id/_vote', function(req, res, next) { + if (req.talk.deleted || req.talk.status === 'scheduled') { + return res.json(400, {code: 'vote_closed', message: 'You can\'t vote anymore for a scheduled talk.', name: 'Bad Request'}); + } + + req.vote = _.extend(new TalkVote(req.params.id + '.' + req.user.member_id), { + _parent: req.params.id, + member_id: req.user.member_id, + }); + + next(); + }); + + app.post('/api/v1/talk/:id/_vote', function(req, res) { + if (!('value' in req.query)) { + return res.json(400, {code: 'missing_parameter', message: 'value parameter is required', name: 'Bad Request'}); + } + + if (~~req.query.value) { + req.vote = req.vote + .import(req.query) + .invoke('save'); + } + else + req.vote = req.vote.delete(); + + req.vote + .then(res.json.bind(res, 200, {ok: true})) + .catch(function(err) { + err.code = err.code || 'es-error'; + res.json(500, err); + }); + }); + + app.delete('/api/v1/talk/:id/_vote', function(req, res) { + req.vote + .delete() + .then(res.json.bind(res, 200, {ok: true})) + .catch(function(err) { + err.code = err.code || 'es-error'; + res.json(500, err); + }); + }); + };