# 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);
+      });
+  });
+
 };