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