Index: Dev/trunk/src/server/app.js
===================================================================
--- Dev/trunk/src/server/app.js	(revision 481)
+++ Dev/trunk/src/server/app.js	(revision 487)
@@ -9,4 +9,7 @@
   , _ = require("underscore")
   , tv4 = require("tv4")
+  , HTTPResult = require("./util/http-result")
+  , etags = require("./util/etags")
+  , cryptoken = require('./util/crypto-token')
   ;
 
@@ -22,6 +25,7 @@
 exports.App = function(settings) {
 
-    assertSetting("couchDbURL", settings, _.isString);
-    var couch = new CouchDB(settings.couchDbURL);
+    assertSetting("couchServerURL", settings, _.isString);
+    assertSetting("dbName", settings, _.isString);
+    var couch = new CouchDB(settings.couchServerURL,settings.dbName);
     
     var schema = require("./config/couchdb-schema.json");
@@ -69,4 +73,6 @@
     app.use(passport.initialize());
     app.use(passport.session());
+    
+    // various middlewares
     function ensureAuthenticated(req,res,next){
         if (!req.user) {
@@ -78,4 +84,7 @@
     function returnUser(req,res) {
         res.send(200, req.user);
+    }
+    function notImplemented(req,res) {
+        res.send(501,{error:"API not implemented yet."});
     }
 
@@ -112,168 +121,669 @@
         });
 
+    var JSON_MIME = 'application/json';
+    var CSV_MIME = 'application/json';
+    function ensureMIME(mimeType) {
+        return function(req,res,next) {
+            if (!req.accepts(mimeType)) {
+                res.send(406);
+            } else {
+                res.set({
+                    'Content-Type': mimeType
+                });
+                next();
+            }
+        };
+    }
+    
+    function stripAndReturnPrivates(obj) {
+        var priv = {};
+        _.each(obj||{},function(val,key){
+            if (key.substring(0,1) === '_') {
+                priv[key] = val;
+                delete obj[key];
+            }
+        });
+        return priv;
+    }
+
+    function identity(obj) { return obj; }
+    function handleUnknownResponse(status,error){
+        return new HTTPResult(500,{error: "Unexpected database response",
+                                   innerStatus: status, innerResponse: error});
+    }
+    function handleUnknownError(error){
+        return new HTTPResult(500, {error: "Unknown error", innerError: error});
+    }
+    function handleRowValues(result) {
+        return _.map(result.rows, function(item) { return item.value; });
+    }
+    function handleRowDocs(result) {
+        return _.map(result.rows, function(item) { return item.doc; });
+    }
+
+    function getDocumentsOfType (type) {
+        var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
+        return HTTPResult.fromResponsePromise(couch.get(url).response,
+                                              handleUnknownError)
+        .handle({
+            200: handleRowValues,
+            404: function() { return {error: "Cannot find collection of type "+type}; },
+            default: handleUnknownResponse
+        });
+    }
+    function getDocument(id,rev,type) {
+        var opts = {headers:{}};
+        if (rev) {
+            opts.headers['If-Non-Match'] = '"'+rev+'"';
+        }
+        return HTTPResult.fromResponsePromise(couch.get(id,opts).response,
+                                              handleUnknownError)
+        .handle({
+            200: function(doc){
+                if ( doc.type !== type ) {
+                    return new HTTPResult(404,{error:"Document not found."});
+                } else {
+                    var priv = stripAndReturnPrivates(doc);
+                    if ( priv._rev !== rev ) {
+                        doc._id = priv._id;
+                        doc._rev = priv._rev;
+                        return doc;
+                    } else {
+                        return new HTTPResult(304);
+                    }
+                }
+            },
+            304: identity,
+            default: handleUnknownResponse
+        });
+    }
+    function putDocument(id,rev,type,doc) {
+        var priv = stripAndReturnPrivates(doc);
+        if ( doc.type === type && tv4.validateResult(doc, schema) ) {
+            var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
+            return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response,
+                                                  handleUnknownError)
+            .handle({
+                201: function(res){
+                    doc._id = res.id;
+                    doc._rev = res.rev;
+                    return doc;
+                },
+                409: function(error) {
+                    return {error: error.reason};
+                },
+                default: handleUnknownResponse
+            });
+        } else {
+            return new HTTPResult(400,{error: "Document failed schema verification."});
+        }
+    }
+    function deleteDocument(id,rev) {
+        if ( rev ) {
+            var opts = {headers:{'If-Match':'"'+rev+'"'}};
+            return HTTPResult.fromResponsePromise(couch.delete(id,opts).response,
+                                                  handleUnknownError)
+            .handle({
+                200: identity,
+                409: function(error) {
+                    return {error: error.reason};
+                },
+                default: handleUnknownResponse
+            });
+        } else {
+            return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
+        }
+    }
+    function postDocument(type,doc) {
+        var priv = stripAndReturnPrivates(doc);
+        if ( doc.type === type && tv4.validateResult(doc, schema) ) {
+            return HTTPResult.fromResponsePromise(couch.post(doc).response,
+                                                  handleUnknownError)
+            .handle({
+                201: function(response) {
+                    doc._id = response.id;
+                    doc._rev = response.rev;
+                    return doc;
+                },
+                default: handleUnknownResponse
+            });
+        } else {
+            return new HTTPResult(400,{error: "Document failed schema verification."});
+        }
+    }
+
     function makeDocsGet(type) {
         return function(req,res) {
-            var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
-            couch.get(url).then(function(result){
-                var items = _.map(result.rows, function(item) { return item.value; });
-                res.send(200, items);
-            }, function(error){
-                res.send(404, {error: "Cannot find collection"});
+            getDocumentsOfType(type)
+            .handle(res.send.bind(res));
+        };
+    }
+    function makeDocGet_id(type) {
+        return function(req,res) {
+            var id = req.params.id;
+            var rev = etags.parse(req.header('If-Non-Match'))[0];
+            getDocument(id,rev,type)
+            .handle({
+                200: function(doc){
+                    res.set({
+                        'ETag': etags.format([doc._rev])
+                    }).send(200, doc);
+                },
+                default: res.send.bind(res)
             });
         };
     }
-    function makeDocGet_id(type) {
+    function makeDocPut_id(type) {
         return function(req,res) {
             var id = req.params.id;
-            couch.get(id).then(function(result){
-                if ( result.type === type ) {
-                    res.send(200, result);
-                } else {
-                    res.send(404, {error: "Document "+id+" is not a "+type});
-                }
-            }, function(error){
-                res.send(404, {error: "Cannot find survey run "+id});
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            putDocument(id,rev,type,doc)
+            .handle({
+                201: function(doc) {
+                    res.set({
+                        'ETag': etags.format([doc._rev])
+                    }).send(201, doc);
+                },
+                default: res.send.bind(res)
             });
         };
     }
-    function makeDocPut_id(type) {
+    function makeDocDel_id(type) {
         return function(req,res) {
             var id = req.params.id;
             var doc = req.body;
-            if ( doc.type === type && tv4.validateResult(doc, schema) ) {
-                couch.put(id,doc).then(function(result){
-                    res.send(200, result);
-                }, function(error){
-                    res.send(500, error);
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            deleteDocument(id,rev)
+            .handle(res.send.bind(res));
+        };
+    }
+    function makeDocPost(type) {
+        return function(req,res) {
+            var doc = req.body;
+            postDocument(type,doc)
+            .handle({
+                201: function(doc) {
+                    res.set({
+                        'ETag': etags.format([doc._rev])
+                    }).send(201, doc);
+                },
+                default: res.send.bind(res)
+            });
+        };
+    }
+
+    // Questions
+    function getQuestionsWithCode(code) {
+        var url = '_design/questions/_view/by_code';
+        var query = {include_docs:true,key:code};
+        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
+                                              handleUnknownError)
+        .handle({
+            200: handleRowDocs,
+            default: handleUnknownResponse
+        });
+    }
+    function getQuestionsWithTopic(topic) {
+        var url = '_design/questions/_view/all_topics';
+        var query = {reduce:false,include_docs:true,key:topic};
+        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
+                                              handleUnknownError)
+        .handle({
+            200: handleRowDocs,
+            default: handleUnknownResponse
+        });
+    }
+    function getQuestionsWithCategoryAndTopic(category,topic) {
+        var hasTopic = typeof topic !== 'undefined';
+        var url = '_design/questions/_view/all';
+        var query = {reduce:false,include_docs:true,
+                     startkey:hasTopic?[category,topic]:[category],
+                     endkey:hasTopic?[category,topic]:[category,{}]};
+        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
+                                              handleUnknownError)
+        .handle({
+            200: handleRowDocs,
+            default: handleUnknownResponse
+        });
+    }
+    app.get('/api/questions',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var hr;
+            if ( 'category' in req.query ) {
+                hr = getQuestionsWithCategoryAndTopic(req.query.category,req.query.topic);
+            } else if ( 'topic' in req.query ) {
+                hr = getQuestionsWithTopic(req.query.topic);
+            } else if ( 'code' in req.query ) {
+                hr = getQuestionsWithCode(req.query.code);
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.post('/api/questions',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function (req,res) {
+            var doc = req.body;
+            getQuestionsWithCode(doc.code)
+            .handle({
+                200: function(others) {
+                    if ( others.length > 0 ) {
+                        return new HTTPResult(403,{error:"Other question with this code already exists."});
+                    } else {
+                        return postDocument('Question',doc);
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.get('/api/questions/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocGet_id('Question'));
+    app.put('/api/questions/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function (req,res) {
+            var id = req.params.id;
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            getQuestionsWithCode(doc.code)
+            .handle({
+                200: function(others) {
+                    if ( others.length > 0 ) {
+                        return new HTTPResult(403,{error:"Other question with this code already exists."});
+                    } else {
+                        return putDocument(id,rev,'Question',doc);
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.delete('/api/questions/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocDel_id('Question'));
+
+
+    // Categories and topics
+    app.get('/api/categories',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var url = '_design/questions/_view/all';
+            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
+                                           handleUnknownError)
+            .handle({
+                200: function(result) {
+                    return _.map(result.rows, function(item) {
+                        return { name:item.key[0], count:item.value };
+                    });
+                },
+                default: handleUnknownResponse
+            })
+            .handle(res.send.bind(res));
+        });
+    app.get('/api/categories/:category/topics',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var category = req.params.category;
+            var url = '_design/questions/_view/all';
+            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
+                                           handleUnknownError)
+            .handle({
+                200: function(result) {
+                    return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
+                },
+                default: handleUnknownResponse
+            })
+            .handle(res.send.bind(res));
+        });
+    app.get('/api/topics',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var url = '_design/questions/_view/all_topics';
+            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
+                                           handleUnknownError)
+            .handle({
+                200: function(result) {
+                    return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
+                },
+                default: handleUnknownResponse
+            })
+            .handle(res.send.bind(res));
+        });
+
+    // Surveys
+    app.get('/api/surveys',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var url;
+            if ( 'drafts' in req.query ) {
+                url = '_design/surveys/_view/drafts';
+            } else if ( 'published' in req.query ) {
+                url = '_design/surveys/_view/published';
+            } else {
+                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
+            }
+            HTTPResult.fromResponsePromise(couch.get(url).response,
+                                           handleUnknownError)
+            .handle({
+                200: function(result) {
+                    return _.map(result.rows, function(item) { return item.value; });
+                },
+                default: handleUnknownResponse
+            })
+            .handle(res.send.bind(res));
+        });
+    app.post('/api/surveys',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocPost('Survey'));
+    app.get('/api/surveys/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocGet_id('Survey'));
+    app.put('/api/surveys/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocPut_id('Survey'));
+    app.delete('/api/surveys/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocDel_id('Survey'));
+
+    // SurveyRuns
+    app.get('/api/surveyRuns',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocsGet('SurveyRun'));
+    app.post('/api/surveyRuns',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var doc = req.body;
+            randomToken()
+            .handle({
+                201: function(token) {
+                    doc.secret = token;
+                    return postDocument('SurveyRun',doc);
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.get('/api/surveyRuns/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocGet_id('SurveyRun'));
+    app.put('/api/surveyRuns/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function (req,res) {
+            var id = req.params.id;
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            var hr;
+            if ( typeof doc.secret === 'undefined' ) {
+                hr = randomToken()
+                .handle({
+                    201: function(token) {
+                        doc.secret = token;
+                        return putDocument(id,rev,'SurveyRun',doc);
+                    }
                 });
             } else {
-                res.send(400,{error: "Document failed schema verification."});
+                hr = putDocument(id,rev,'SurveyRun',doc);
             }
-        };
-    }
-    function makeDocDel_id(type) {
-        return function(req,res) {
-            var id = req.params.id;
-            var rev = req.query.rev;
-            couch.delete(id,rev).then(function(result){
-                res.send(200, result);
-            }, function(error){
-                res.send(500, error);
+            hr.handle(res.send.bind(res));
+        });
+    app.delete('/api/surveyRuns/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocDel_id('SurveyRun'));
+    app.get('/api/surveyRuns/:id/responses',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var id = req.params.id;
+            getResponsesBySurveyRunId(id)
+            .handle(res.send.bind(res));
+        });
+    app.get('/api/surveyRuns/:id/responses.csv',
+        ensureAuthenticated,
+        ensureMIME(CSV_MIME),
+        function(req, res) {
+            var id = req.params.id;
+            getResponsesBySurveyRunId(id)
+            .handle({
+                200: function(responses) {
+                    var flatResponses = responsesToVariables(responses);
+                    res.set({
+                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
+                    });
+                    res.status(200);
+                    writeObjectsToCSVStream(flatResponses, res);
+                    res.end();
+                },
+                default: res.send.bind(res)
             });
-        };
-    }
-    function makeDocPost(type) {
-        return function(req,res) {
-            var doc = req.body;
-            if ( doc.type === type && tv4.validateResult(doc, schema) ) {
-                couch.post(req.body).then(function(result){
-                    res.send(200, result);
-                }, function(error){
-                    res.send(500, error);
+        });
+    
+    // Responses
+    function getResponsesBySurveyRunId(surveyRunId) {
+        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
+        return HTTPResult.fromResponsePromise(couch.get(url).response,
+                                              handleUnknownError)
+        .handle({
+            200: handleRowValues,
+            default: handleUnknownResponse
+        });
+    }
+    app.get('/api/responses',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res){
+            var hr;
+            if ( 'surveyRunId' in req.query ) {
+                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
+            } else {
+                hr = getDocumentsOfType('Response');
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.get('/api/responses/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocGet_id('Response'));
+    app.post('/api/responses',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function (req,res) {
+            var doc = req.body;
+            randomToken()
+            .handle({
+                201: function(token) {
+                    doc.secret = token;
+                    return postDocument('Response',doc);
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.put('/api/responses/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function (req,res) {
+            var id = req.params.id;
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            var hr;
+            if ( typeof doc.secret === 'undefined' ) {
+                hr = randomToken()
+                .handle({
+                    201: function(token) {
+                        doc.secret = token;
+                        return putDocument(id,rev,'Response',doc);
+                    }
                 });
             } else {
-                res.send(400,{error: "Document failed schema verification."});
+                hr = putDocument(id,rev,'Response',doc);
             }
-        };
-    }
-    function notImplemented(req,res) {
-        res.send(501,{error:"API not implemented yet."});
-    }
-
-    // Questions
-    app.get('/api/questions',
-        ensureAuthenticated,
-        makeDocsGet('Question'));
-    app.get('/api/questions/:id',
-        ensureAuthenticated,
-        makeDocGet_id('Question'));
-    app.post('/api/questions',
-        ensureAuthenticated,
-        makeDocPost('Question'));
-    app.put('/api/questions/:id',
-        ensureAuthenticated,
-        makeDocPut_id('Question'));
-    app.delete('/api/questions/:id',
-        ensureAuthenticated,
-        makeDocDel_id('Question'));
-    app.get('/api/questions/categories',
-        ensureAuthenticated,
-        notImplemented);
-
-    // Surveys
-    app.get('/api/surveys',
-        ensureAuthenticated,
-        makeDocsGet('Survey'));
-    app.get('/api/surveys/:id',
-        ensureAuthenticated,
-        makeDocGet_id('Survey'));
-    app.post('/api/surveys',
-        ensureAuthenticated,
-        makeDocPost('Survey'));
-    app.put('/api/surveys/:id',
-        ensureAuthenticated,
-        makeDocPut_id('Survey'));
-    app.delete('/api/surveys/:id',
-        ensureAuthenticated,
-        makeDocDel_id('Survey'));
-
-    // SurveyRuns
-    app.get('/api/surveyRuns',
-        ensureAuthenticated,
-        makeDocsGet('SurveyRun'));
-    app.get('/api/surveyRuns/:id',
-        ensureAuthenticated,
-        makeDocGet_id('SurveyRun'));
-    app.get('/api/surveyRuns/:id/responses.csv',
-        ensureAuthenticated,
-        function(req, res) {
-            var id = req.params.id;
-            var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id);
-            couch.get(url).then(function(result){
-                var responses = _.map(result.rows, function(item) { return item.value; });
-                var flatResponses = responsesToVariables(responses);
-                res.set({
-                    'Content-Type': 'text/csv',
-                    'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
-                });
-                res.status(200);
-                writeObjectsToCSVStream(flatResponses, res);
-                res.end();
-            }, function(error){
-                res.send(404, {error: "Cannot find responses for survey run "+id});
-            });
-        });
-    app.get('/api/surveyRuns/:id/responses',
-        ensureAuthenticated,
+            hr.handle(res.send.bind(res));
+        });
+    app.delete('/api/responses/:id',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        makeDocDel_id('Response'));
+
+    //respondents api
+    function isSurveyRunRunning(surveyRun) {
+        var now = new Date();
+        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
+        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
+        return afterStart && beforeEnd;
+    }
+    app.get('/api/open/responses/:id',
+        ensureMIME(JSON_MIME),
         function(req,res) {
             var id = req.params.id;
-            var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id);
-            couch.get(url).then(function(result){
-                var responses = _.map(result.rows, function(item) { return item.value; });
-                res.send(200, responses);
-            }, function(error){
-                res.send(404, {error: "Cannot find responses for survey run "+id});
-            });
-        });
-    
-    // Responses
-    app.get('/api/responses',
-        ensureAuthenticated,
-        makeDocsGet('Response'));
-    app.get('/api/responses/:id',
-        ensureAuthenticated,
-        makeDocGet_id('Response'));
-    app.post('/api/responses',
-        ensureAuthenticated,
-        makeDocPost('Response'));
-    app.put('/api/responses/:id',
-        ensureAuthenticated,
-        makeDocPut_id('Response'));
-    app.delete('/api/responses/:id',
-        ensureAuthenticated,
-        makeDocDel_id('Response'));
+            var rev = etags.parse(req.header('If-Non-Match'))[0];
+            var secret = req.query.secret;
+            getDocument(id,rev,'Response')
+            .handle({
+                200: function(response) {
+                    if ( response.secret === secret ) {
+                        return getDocument(response.surveyRunId,[],'SurveyRun')
+                        .handle({
+                            200: function(surveyRun) {
+                                if ( !isSurveyRunRunning(surveyRun) ) {
+                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
+                                } else {
+                                    response._surveyRun = surveyRun;
+                                    return response;
+                                }
+                            }
+                        });
+                    } else {
+                        return new HTTPResult(403,{error: "Wrong secret for response"});
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.post('/api/open/responses',
+        ensureMIME(JSON_MIME),
+        function(req,res) {
+            var secret = req.query.secret;
+            var response = req.body;
+            delete response._surveyRun;
+            getDocument(response.surveyRunId,[],'SurveyRun')
+            .handle({
+                200: function(surveyRun) {
+                    if ( surveyRun.secret !== secret ) {
+                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
+                    } else if ( !isSurveyRunRunning(surveyRun) ) {
+                        return new HTTPResult(404,{error:"Survey is not running anymore."});
+                    } else if ( surveyRun.mode === 'closed' ) {
+                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
+                    } else {
+                        return randomToken()
+                        .handle({
+                            201: function(token) {
+                                response.secret = token;
+                                return postDocument('Response',response)
+                                .handle({
+                                    201: function(doc){
+                                        doc._surveyRun = surveyRun;
+                                        return doc;
+                                    }
+                                });
+                            }
+                        });
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.put('/api/open/responses/:id',
+        ensureMIME(JSON_MIME),
+        function(req,res){
+            var id = req.params.id;
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            var secret = req.query.secret || doc.secret;
+            delete doc._surveyRun;
+            getDocument(id,[],'Response')
+            .handle({
+                200: function(prev) {
+                    if ( prev.secret !== secret ) {
+                        return new HTTPResult(403,{error: "Secrets are not the same."});
+                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
+                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
+                    } else {
+                        doc.secret = secret; // restore in case it got lost or was changed
+                        return getDocument(doc.surveyRunId,[],'SurveyRun')
+                        .handle({
+                            200: function(surveyRun) {
+                                if ( !isSurveyRunRunning(surveyRun) ) {
+                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
+                                } else {
+                                    return putDocument(id,rev,'Response',doc)
+                                    .handle({
+                                        200: function(doc) {
+                                            doc._surveyRun = surveyRun;
+                                            return new HTTPResult(201,doc);
+                                        }
+                                    });
+                                }
+                            }
+                        });
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.delete('/api/open/responses/:id',
+        ensureMIME(JSON_MIME),
+        function(req,res){
+            var id = req.params.id;
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            var secret = req.query.secret || doc.secret;
+            delete doc._surveyRun;
+            getDocument(id,[],'Response')
+            .handle({
+                200: function(prev) {
+                    if ( prev.secret !== secret ) {
+                        return new HTTPResult(403,{error: "Secrets are not the same."});
+                    } else {
+                        return deleteDocument(id,rev,doc);
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+
+    // uuids
+    app.get('/api/uuids',
+        ensureAuthenticated,
+        ensureMIME(JSON_MIME),
+        function(req,res){
+            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
+            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
+                                           handleUnknownError)
+            .handle({
+                200: function(res) {
+                    return res.uuids;
+                },
+                default: handleUnknownResponse
+            })
+            .handle(res.send.bind(res));
+        });
 
     return app;
-
 }
 
@@ -341,2 +851,13 @@
     });
 }
+
+function randomToken() {
+    var result = new HTTPResult();
+    cryptoken(8)
+    .then(function(token){
+        result.set(201,token);
+    }, function(ex){
+        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
+    });
+    return result;
+}
Index: Dev/trunk/src/server/bin/check-db.js
===================================================================
--- Dev/trunk/src/server/bin/check-db.js	(revision 481)
+++ Dev/trunk/src/server/bin/check-db.js	(revision 487)
@@ -1,5 +1,6 @@
 var env = require('../env')
-  , checkCouch = require('../config/check-couchdb');
-checkCouch(env.couchDbURL)
+  , checkCouch = require('../config/check-couchdb')
+  ;
+checkCouch(env.couchServerURL,env.dbName)
 .then(function(res){
     console.log("done",res);
Index: Dev/trunk/src/server/bin/config-db-for-upgrade.js
===================================================================
--- Dev/trunk/src/server/bin/config-db-for-upgrade.js	(revision 487)
+++ Dev/trunk/src/server/bin/config-db-for-upgrade.js	(revision 487)
@@ -0,0 +1,10 @@
+var env = require('../env')
+  , configCouch = require('../config/config-couchdb')
+  , designDocs = require('../config/couchdb-upgrade-design-docs')
+  ;
+configCouch(env.couchServerURL,env.dbName,designDocs)
+.then(function(res){
+    console.log("done",res);
+}, function(err){
+    console.error("fail",err);
+});
Index: Dev/trunk/src/server/bin/config-db.js
===================================================================
--- Dev/trunk/src/server/bin/config-db.js	(revision 481)
+++ Dev/trunk/src/server/bin/config-db.js	(revision 487)
@@ -1,5 +1,7 @@
 var env = require('../env')
-  , checkCouch = require('../config/config-couchdb');
-checkCouch(env.couchServerURL)
+  , configCouch = require('../config/config-couchdb')
+  , designDocs = require('../config/couchdb-design-docs')
+  ;
+configCouch(env.couchServerURL,env.dbName,designDocs)
 .then(function(res){
     console.log("done",res);
Index: Dev/trunk/src/server/bin/heroku.js
===================================================================
--- Dev/trunk/src/server/bin/heroku.js	(revision 481)
+++ Dev/trunk/src/server/bin/heroku.js	(revision 487)
@@ -1,11 +1,14 @@
-var env = require('../env');
-var configCouch = require('../config/config-couchdb');
+var env = require('../env')
+  , configCouch = require('../config/config-couchdb')
+  , designDocs = require('../config/config-couchdb')
+  ;
 
 console.log("Running on",env.couchServerURL);
 
 require('../app').App({
-    couchDbURL: env.couchDbURL
+    couchServerURL: env.couchServerURL,
+    dbName: env.dbName
 }).then(function(app){
-    configCouch(env.couchServerURL);
+    configCouch(env.couchServerURL,env.dbName,designDocs);
     return app;
 }).then(function(app){
Index: Dev/trunk/src/server/bin/upgrade-db-1-to-2.js
===================================================================
--- Dev/trunk/src/server/bin/upgrade-db-1-to-2.js	(revision 487)
+++ Dev/trunk/src/server/bin/upgrade-db-1-to-2.js	(revision 487)
@@ -0,0 +1,19 @@
+var env = require('../env')
+  , upgradeCouch = require('../config/upgrade-couchdb')
+  , cryptoken = require('../util/crypto-token')
+  , Q = require('q')
+  ;
+upgradeCouch(env.couchServerURL,env.dbName,function(doc){
+    if ( (doc.type === 'Response' || doc.type === 'SurveyRun') && !doc.secret ) {
+        return cryptoken(8).then(function(token){
+            doc.secret = token;
+            return doc;
+        });
+    } else {
+        return Q.reject();
+    }
+}).then(function(res){
+    console.log("done",res);
+}, function(err){
+    console.error("fail",err);
+});
Index: Dev/trunk/src/server/config/check-couchdb.js
===================================================================
--- Dev/trunk/src/server/config/check-couchdb.js	(revision 481)
+++ Dev/trunk/src/server/config/check-couchdb.js	(revision 487)
@@ -7,6 +7,6 @@
 var schema = require('./couchdb-schema');
 
-module.exports = function(couchDbURL) {
-    var server = new CouchDB(couchDbURL);
+module.exports = function(couchServerURL,dbName) {
+    var server = new CouchDB(couchServerURL,dbName);
     var designRe = /^_design\//;
     return server.get('/_all_docs')
Index: Dev/trunk/src/server/config/config-couchdb.js
===================================================================
--- Dev/trunk/src/server/config/config-couchdb.js	(revision 481)
+++ Dev/trunk/src/server/config/config-couchdb.js	(revision 487)
@@ -5,8 +5,8 @@
   ;
 
-var designDocs = require('./couchdb-design-docs');
+module.exports = function(couchServerURL, dbName, designDocs) {
+    var server = new CouchDB(couchServerURL);
 
-module.exports = function(couchServerURL) {
-    var server = new CouchDB(couchServerURL);
+    if ( !designDocs ) { throw new Error("Forgot to pass design docs to checkCouch."); }
 
     console.log("Configuring CouchDB for QED");
@@ -14,8 +14,8 @@
     return server.get('')
     .then(function(info){
-        if (info.version !== "1.2.0" ) {
+        if (info.version !== "1.4.0" ) {
             console.log("Found "+info.version+", only tested with CouchDB 1.2.0");
         } else {
-            console.log("CouchDB 1.2.0 found");
+            console.log("CouchDB 1.4.0 found");
         }
     }).then(function(res){
Index: Dev/trunk/src/server/config/couchdb-design-docs.js
===================================================================
--- Dev/trunk/src/server/config/couchdb-design-docs.js	(revision 481)
+++ Dev/trunk/src/server/config/couchdb-design-docs.js	(revision 487)
@@ -11,5 +11,5 @@
 
     "qed/schemaInfo": {
-        version: "1"
+        version: "2"
     },
 
@@ -31,5 +31,7 @@
         language: "javascript",
         validate_doc_update: function(newDoc, oldDoc, userCtx, secObj) {
-            if ( oldDoc && oldDoc.publicationDate ) { throw({forbidden:'Published documents cannot be modified.'}); }
+            if ( oldDoc && oldDoc.publicationDate ) {
+                throw({forbidden:'Published documents cannot be modified.'});
+            }
         },
         views: {
@@ -59,8 +61,8 @@
                     if ( doc.categories && doc.categories.length > 0 ) {
                         for ( var i = 0; i < doc.categories.length; i++ ) { 
-                            emit([doc.categories[i],doc.topic||null],1);
+                            emit([doc.categories[i],doc.topic||"(default)"],1);
                         } 
                     } else { 
-                        emit([null,null],1); 
+                        emit(["(default)","(default)"],1); 
                     } 
                 },
@@ -72,8 +74,8 @@
                     if ( doc.categories && doc.categories.length > 0 ) {
                         for ( var i = 0; i < doc.categories.length; i++ ) {
-                            emit([doc.categories[i],doc.topic||null],1);
+                            emit([doc.categories[i],doc.topic||"(default)"],1);
                         }
                     } else {
-                        emit([null,null],1);
+                        emit(["(default)","(default)"],1);
                     }
                 },
@@ -83,14 +85,20 @@
                 map: function(doc){
                     if( doc.type !== 'Question' ){ return; }
-                    emit(doc.topic);
+                    emit(doc.topic||"(default)",1);
                 },
-                reduce: function(key, values, rereduce) { return null; }
+                reduce: function(key, values, rereduce) { return sum(values); }
             },
             published_topics: {
                 map: function(doc){
                     if ( doc.type !== 'Question' || !doc.publicationDate ) { return; }
-                    emit(doc.topic);
+                    emit(doc.topic||"(default)",1);
                 },
-                reduce: function(key, values, rereduce) { return null; }
+                reduce: function(key, values, rereduce) { return sum(values); }
+            },
+            by_code: {
+                map: function(doc){
+                    if ( doc.type !== 'Question' ) { return; }
+                    emit(doc.code,doc);
+                }
             }
         }
Index: Dev/trunk/src/server/config/couchdb-schema.json
===================================================================
--- Dev/trunk/src/server/config/couchdb-schema.json	(revision 481)
+++ Dev/trunk/src/server/config/couchdb-schema.json	(revision 487)
@@ -1,129 +1,144 @@
 {
+  "$schema": "http://json-schema.org/draft-04/schema#",
   "title": "QED Object Schema",
-  "version": "1",
+  "version": "2",
   "type": "object",
   "oneOf": [
-    { "$ref": "#/schemaInfo" },
-    { "$ref": "#/docTypes/any" }
+    { "$ref": "#/definitions/schemaInfo" },
+    { "$ref": "#/definitions/docs/any" }
   ],
-  "schemaInfo": {
-    "type": "object",
-    "properties": {
-        "_id": { "type": "string", "pattern": "schemaInfo" },
-        "_rev": { "type": "string", "optional": true },
-        "version": { "type": "string" }
-    },
-    "additionalProperties": false
-  },
-  "docTypes": {
-    "any": {
+  "definitions": {
+    "schemaInfo": {
       "type": "object",
-      "oneOf": [
-        { "$ref": "#/docTypes/Question" },
-        { "$ref": "#/docTypes/Survey" },
-        { "$ref": "#/docTypes/SurveyRun" },
-        { "$ref": "#/docTypes/Response" }
-      ]
-    },
-    "Question": {
       "properties": {
-        "type": { "type": "string", "pattern": "Question" },
-        "_id": { "type": "string", "optional": true },
-        "_rev": { "type": "string", "optional": true },
-        "categories": { "type": "array", "items": { "type": "string" } },
-        "code": { "type": "string" },
-        "content": { "type": "array", "items": { "$ref": "#/contentTypes/any" } },
-        "description": { "type": "string", "optional": true },
-        "publicationDate": { "type": "string", "pattern": "", "optional": true },
-        "title": { "type": "string" },
-        "topic": { "type": "string", "optional": true }
+          "_id": { "type": "string", "pattern": "schemaInfo" },
+          "_rev": { "type": "string" },
+          "version": { "type": "string" }
       },
+      "required": ["_id","version"],
       "additionalProperties": false
     },
-    "Survey": {
-      "type": "object",
-      "properties": {
-        "type": { "type": "string", "pattern": "Survey" },
-        "_id": { "type": "string", "optional": true },
-        "_rev": { "type": "string", "optional": true },
-        "publicationDate": { "type": "string", "pattern": "", "optional": true },
-        "questions": { "type": "array", "items": { "$ref": "#/docTypes/Question" } },
-        "title": { "type": "string" }
+    "docs": {
+      "any": {
+        "type": "object",
+        "oneOf":[
+          { "$ref": "#/definitions/docs/Question" },
+          { "$ref": "#/definitions/docs/Survey" },
+          { "$ref": "#/definitions/docs/SurveyRun" },
+          { "$ref": "#/definitions/docs/Response" }
+        ]
       },
-      "additionalProperties": false
-    },
-    "SurveyRun": {
-      "type": "object",
-      "properties": {
-        "type": { "type": "string", "pattern": "SurveyRun" },
-        "_id": { "type": "string", "optional": true },
-        "_rev": { "type": "string", "optional": true },
-        "description": { "type": "string" },
-        "endDate": { "type": "string", "pattern": "" },
-        "mode": { "type": "string", "enum": [ "open", "closed" ] },
-        "startDate": { "type": "string", "pattern": "" },
-        "survey": { "$ref": "#/docTypes/Survey" },
-        "title": { "type": "string" }
-      },
-      "additionalProperties": false
-    },
-    "Response": {
-      "type": "object",
-      "properties": {
-        "type": { "type": "string", "pattern": "Response" },
-        "_id": { "type": "string", "optional": true },
-        "_rev": { "type": "string", "optional": true },
-        "answers": { "type": "object" },
-        "publicationDate": { "type": "string", "pattern": "", "optional": true },
-        "surveyRunId": { "type": "string" }
-      },
-      "additionalProperties": false
-    }
-  },
-  "contentTypes": {
-    "any": {
-      "type": "object",
-      "oneOf": [
-        { "$ref": "#/contentTypes/Text" },
-        { "$ref": "#/contentTypes/StringInput" },
-        { "$ref": "#/contentTypes/ScaleInput" }
-      ]
-    },
-    "Text": {
+      "Question": {
         "type": "object",
         "properties": {
-            "type": { "type": "string", "pattern": "Text" },
-            "text": { "type": "string" }
+          "type": { "type": "string", "pattern": "Question" },
+          "_id": { "type": "string" },
+          "_rev": { "type": "string" },
+          "categories": { "type": "array", "items": { "type": "string" } },
+          "code": { "type": "string" },
+          "content": { "type": "array", "items": { "$ref": "#/definitions/content/any" } },
+          "description": { "type": "string" },
+          "publicationDate": { "type": "string", "format": "datetime" },
+          "title": { "type": "string" },
+          "topic": { "type": "string" }
         },
+        "required": ["type","categories","code","content","title"],
         "additionalProperties": false
-    },
-    "StringInput": {
+      },
+      "Survey": {
         "type": "object",
         "properties": {
-            "type": { "type": "string", "pattern": "StringInput" },
-            "text": { "type": "string" }
+          "type": { "type": "string", "pattern": "Survey" },
+          "_id": { "type": "string" },
+          "_rev": { "type": "string" },
+          "publicationDate": { "type": "string", "format": "datetime" },
+          "questions": { "type": "array", "items": { "$ref": "#/definitions/docs/Question" } },
+          "title": { "type": "string" }
         },
+        "required": ["type","questions","title"],
         "additionalProperties": false
-    },
-    "ScaleInput": {
+      },
+      "SurveyRun": {
         "type": "object",
         "properties": {
-            "type": { "type": "string", "pattern": "ScaleInput" },
-            "minLabel": { "type": "string", "optional": true },
-            "min": { "type": "integer" },
-            "max": { "type": "integer" },
-            "maxLabel": { "type": "string", "optional": true },
-            "naLabel": { "type": "string", "optional": true },
-            "items": { "type": "array", "items": {
-                "type": "object",
-                "properties": {
-                    "text": { "type": "string" },
-                    "minLabel": { "type": "string", "optional": true },
-                    "maxLabel": { "type": "string", "optional": true }
-                },
-                "additionalProperties": false
-            } }
+          "type": { "type": "string", "pattern": "SurveyRun" },
+          "_id": { "type": "string" },
+          "_rev": { "type": "string" },
+          "description": { "type": "string" },
+          "endDate": { "type": "string", "format": "datetime" },
+          "mode": { "type": "string", "enum": [ "open", "closed" ] },
+          "secret": { "type": "string", "minLength": 8 },
+          "startDate": { "type": "string", "format": "datetime" },
+          "survey": { "$ref": "#/definitions/docs/Survey" },
+          "title": { "type": "string" }
         },
+        "required": ["type","description","mode","secret","survey","title"],
         "additionalProperties": false
+      },
+      "Response": {
+        "type": "object",
+        "properties": {
+          "type": { "type": "string", "pattern": "Response" },
+          "_id": { "type": "string" },
+          "_rev": { "type": "string" },
+          "answers": { "type": "object" },
+          "publicationDate": { "type": "string", "format": "datetime" },
+          "secret": { "type": "string", "minLength": 8 },
+          "surveyRunId": { "type": "string" }
+        },
+        "required": ["type","answers","secret","surveyRunId"],
+        "additionalProperties": false
+      }
+    },
+    "content":{
+      "any": {
+        "type": "object",
+        "oneOf": [
+          { "$ref": "#/definitions/content/Text" },
+          { "$ref": "#/definitions/content/StringInput" },
+          { "$ref": "#/definitions/content/ScaleInput" }
+        ]
+      },
+      "Text": {
+        "type": "object",
+        "properties": {
+          "type": { "type": "string", "pattern": "Text" },
+          "text": { "type": "string" }
+        },
+        "required": ["type","text"],
+        "additionalProperties": false
+      },
+      "StringInput": {
+        "type": "object",
+        "properties": {
+          "type": { "type": "string", "pattern": "StringInput" },
+          "text": { "type": "string" }
+        },
+        "required":["type","text"],
+        "additionalProperties": false
+      },
+      "ScaleInput": {
+        "type": "object",
+        "properties": {
+          "type": { "type": "string", "pattern": "ScaleInput" },
+          "minLabel": { "type": "string" },
+          "min": { "type": "integer" },
+          "max": { "type": "integer" },
+          "maxLabel": { "type": "string" },
+          "naLabel": { "type": "string" },
+          "items": { "type": "array", "items": {
+            "type": "object",
+            "properties": {
+              "text": { "type": "string" },
+              "minLabel": { "type": "string" },
+              "maxLabel": { "type": "string" }
+            },
+            "required":["text"],
+            "additionalProperties": false
+          } }
+        },
+        "required":["type","min","max","items"],
+        "additionalProperties": false
+      }
     }
   }
Index: Dev/trunk/src/server/config/couchdb-upgrade-design-docs.js
===================================================================
--- Dev/trunk/src/server/config/couchdb-upgrade-design-docs.js	(revision 487)
+++ Dev/trunk/src/server/config/couchdb-upgrade-design-docs.js	(revision 487)
@@ -0,0 +1,11 @@
+module.exports = {
+
+    "qed/schemaInfo": {
+        version: "-1"
+    },
+
+    "qed/_design/default": {
+        __configAction: "replace"
+    }
+
+};
Index: Dev/trunk/src/server/config/upgrade-couchdb.js
===================================================================
--- Dev/trunk/src/server/config/upgrade-couchdb.js	(revision 487)
+++ Dev/trunk/src/server/config/upgrade-couchdb.js	(revision 487)
@@ -0,0 +1,38 @@
+var Q = require('q')
+  , _ = require('underscore')
+  , tv4 = require('tv4')
+  , CouchDB = require('../util/couch').CouchDB
+  ;
+
+var schema = require('./couchdb-schema');
+
+module.exports = function(couchServerURL,dbName,upgrade) {
+    var server = new CouchDB(couchServerURL,dbName);
+    var designRe = /^_design\//;
+    return server.get('/_all_docs')
+    .then(function(allDocs){
+        return _.chain(allDocs.rows)
+        .filter(function(doc){
+            return doc.id.match(designRe) === null;
+        })
+        .reduce(function(memo, doc){
+            return memo.then(function(result){
+                return server.get(doc.id)
+                .then(function(doc){
+                    Q.when(upgrade(doc))
+                    .then(function(newDoc){
+                        return server.put(doc._id,newDoc)
+                        .then(function(){
+                            result[doc._id] = true;
+                            return result;
+                        });
+                    },function(){
+                        result[doc._id] = false;
+                    });
+                    return result;
+                });
+            });
+        }, Q.resolve({}))
+        .value();
+    }).all();
+};
Index: Dev/trunk/src/server/env.js
===================================================================
--- Dev/trunk/src/server/env.js	(revision 481)
+++ Dev/trunk/src/server/env.js	(revision 487)
@@ -3,4 +3,4 @@
     port: process.env.PORT || 5000,
     couchServerURL: couchServerURL,
-    couchDbURL: couchServerURL+'qed',
+    dbName: 'qed'
 };
Index: Dev/trunk/src/server/util/couch.coffee
===================================================================
--- Dev/trunk/src/server/util/couch.coffee	(revision 481)
+++ Dev/trunk/src/server/util/couch.coffee	(revision 487)
@@ -6,32 +6,42 @@
  
 class CouchDB
-    constructor: (url) ->
-        @url = normalizeURL url
-    get: (id) ->
+    constructor: (url,db) ->
+        @serverURL = normalizeURL url
+        @db = db
+        if @db
+            @url = "#{@serverURL}/#{@db}"
+        else
+            @url = "#{@serverURL}"
+    get: (id, opts) ->
         url = "#{@url}/#{id}"
-        couchRequest 'GET', url
-    post: (doc) ->
-        url = "#{@url}"
-        couchRequest 'POST', url, doc
-    put: (id, doc) ->
+        couchRequest 'GET', url, null, opts
+    post: (doc, opts) ->
+        url = "#{@url}/"
+        couchRequest 'POST', url, doc, opts
+    put: (id, doc, opts) ->
         url = "#{@url}/#{id}"
-        couchRequest 'PUT', url, doc
-    delete: (id, rev) ->
-        url = "#{@url}/#{id}?rev=#{rev}"
-        couchRequest 'DELETE', url
+        couchRequest 'PUT', url, doc, opts
+    delete: (id, opts) ->
+        url = "#{@url}/#{id}"
+        couchRequest 'DELETE', url, null, opts
+    uuids: (opts) ->
+        url = "#{@serverURL}/_uuids"
+        couchRequest 'GET', url, null, opts
 
 normalizeURL = (url) ->
     url.replace /\/$/, ''
 
-couchRequest = (method, url, data) ->
+couchRequest = (method, url, body, opts) ->
     options =
         method: method,
         headers:
-            'content-type': 'application/json; charset=utf-8'
-            'accept': 'application/json'
-        body: JSON.stringify (stringifyFunctions (data || {}))
+            'Content-Type': 'application/json; charset=utf-8'
+            'Accept': 'application/json'
+        body: JSON.stringify (stringifyFunctions (body || {}))
+    if opts
+        options.qs = createQueryObj opts.query if opts.query
+        _.extend options.headers, opts.headers if opts.headers
     req = request(url, options)
-    req.response
-    .then (res) =>
+    res = req.response.then (res) =>
         req.then (res) =>
             JSON.parse res
@@ -40,5 +50,18 @@
     , (err) =>
         Q.reject err
+    res.response = req.response.then (res) =>
+        statusCode: res.statusCode
+        headers: res.headers
+        body: JSON.parse res.body
+    , (err) =>
+        Q.reject err
+    res
 
+createQueryObj = (obj) ->
+    newObj = {}
+    _.each obj, (val,key) ->
+        newObj[key] = JSON.stringify val
+    newObj
+        
 stringifyFunctions = (value) ->
     if value is null
Index: Dev/trunk/src/server/util/crypto-token.js
===================================================================
--- Dev/trunk/src/server/util/crypto-token.js	(revision 487)
+++ Dev/trunk/src/server/util/crypto-token.js	(revision 487)
@@ -0,0 +1,14 @@
+var Q = require('q')
+  , crypto = require('crypto');
+
+module.exports = function(bytes) {
+    var q = Q.defer();
+    crypto.randomBytes(bytes || 8, function(ex, buf) {
+        if ( ex ) {
+            q.reject(ex);
+        } else {
+            q.resolve(buf.toString('hex'));
+        }
+    });
+    return q.promise;
+};
Index: Dev/trunk/src/server/util/etags.js
===================================================================
--- Dev/trunk/src/server/util/etags.js	(revision 487)
+++ Dev/trunk/src/server/util/etags.js	(revision 487)
@@ -0,0 +1,14 @@
+var _ = require('underscore');
+
+var revRe = /"([^"]*)"/g;
+
+exports.parse = function(header) {
+    return _.map((header && header.match(revRe)) || [], function(str){
+        return str.substring(1,str.length-1);
+    });
+};
+
+exports.format = function(etags) {
+    return _.map(etags || [], function(str) { return '"'+str+'"'; })
+            .join(', ');
+};
Index: Dev/trunk/src/server/util/http-result.js
===================================================================
--- Dev/trunk/src/server/util/http-result.js	(revision 487)
+++ Dev/trunk/src/server/util/http-result.js	(revision 487)
@@ -0,0 +1,78 @@
+// <init> :: status? -> result? -> HTTPResult
+function HTTPResult(status,result) {
+    this.status = 0;
+    this.result = null;
+    this._handlers = [];
+    if ( status ) {
+        this.set(status,result);
+    }
+}
+
+HTTPResult.fromResponsePromise = function(promise,errorHandler) {
+    var result = new HTTPResult();
+    promise.then(function(response){
+        result.set(response.statusCode,response.body);
+    }, errorHandler && function(error){
+        var ret = errorHandler(error);
+        if ( ret.constructor === HTTPResult ) {
+            ret.handle(result.set.bind(result));
+        } else {
+            result.set(500,ret);
+        }
+    });
+    return result;
+};
+
+// isSet :: Bool
+HTTPResult.prototype.isSet = function() {
+    return this.status !== 0;
+};
+
+// set :: status -> result? -> ()
+HTTPResult.prototype.set = function(status,result) {
+    if ( this.isSet() ) { throw new Error("Result was already set."); }
+    if ( typeof status !== 'number' ) { throw new Error("Status must be a number."); }
+    this.status = status;
+    this.result = result;
+    for ( var i = 0; i < this._handlers.length; i++ ) {
+        this._fire(this._handlers[i]);
+    }
+};
+
+// _fire :: (status -> result -> Either HTTPResult Any) -> ()
+HTTPResult.prototype._fire = function(f) {
+    var ret = f(this.status,this.result);
+    if ( ret.constructor === HTTPResult ) {
+        ret.handle(f._next.set.bind(f._next));
+    } else {
+        f._next.set(this.status,ret);
+    }
+};
+
+/* handle :: Either (status -> result -> Either HTTPResult Any)
+ *                  {number: (result -> Either HTTPResult Any),
+ *                   'default': (status -> result -> Either HTTPResult Any)}
+ *           -> HTTPResult
+ */
+HTTPResult.prototype.handle = function(fOrObj) {
+    var f = typeof fOrObj === 'function' ?
+            fOrObj :
+            function(status,result) {
+                if ( status in fOrObj ) {
+                    return fOrObj[status](result);
+                } else if ( 'default' in fOrObj ) {
+                    return fOrObj['default'](status,result);
+                } else {
+                    return result;
+                }
+            };
+    f._next = new HTTPResult();
+    if ( this.isSet() ) {
+        this._fire(f);
+    } else {
+        this._handlers.push(f);
+    }
+    return f._next;
+};
+
+module.exports = HTTPResult;
