Index: Dev/trunk/src/server/app.js
===================================================================
--- Dev/trunk/src/server/app.js	(revision 474)
+++ Dev/trunk/src/server/app.js	(revision 475)
@@ -1,9 +1,12 @@
-var express = require("express");
-var passport = require("passport"),
-    passportLocal = require("passport-local");
-var fs = require("fs");
-var path = require("path");
-var proxy = require("./util/simple-http-proxy");
-var _ = require("underscore");
+var express = require("express")
+  , passport = require("passport")
+  , passportLocal = require("passport-local")
+  , fs = require("fs")
+  , path = require("path")
+  , proxy = require("./util/simple-http-proxy")
+  , CSV = require("ya-csv")
+  , CouchDB = require('./util/couch').CouchDB
+  , _ = require("underscore")
+  ;
 
 function assertSetting(name, settings, validate) {
@@ -19,4 +22,5 @@
 
     assertSetting("couchDbURL", settings, _.isString);
+    var couch = new CouchDB(settings.couchDbURL);
     
     function clientPath(relativePath) {
@@ -105,10 +109,18 @@
         function(req, res) {
             var id = req.params.id;
-            // query CouchDB and build the CSV file
-            res.set({
-                'Content-Type': 'text/csv',
-                'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
+            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});
             });
-            res.send(200, "Response for surveyRun "+id);
         });
     
@@ -116,2 +128,51 @@
 
 };
+
+function responsesToVariables(responses) {
+    return _.map(responses, responseToVariables);
+}
+
+function responseToVariables(response) {
+    var result = flattenObject(response.answers);
+    result.respondent = response.id;
+    return result;
+}
+
+function flattenObject(value) {
+    var result = {};
+    (function agg(val,key,res){
+        if ( _.isObject(val) ) {
+            _.forEach(val, function(v,k){
+                agg(v,(key ? key+'.' : '')+k,res);
+            });
+        } else if ( _.isArray(val) ) {
+            _.forEach(val, function(v,i){
+                agg(v,(key ? key+'.' : '')+i,res);
+            });
+        } else {
+            res[key] = val;
+        }
+    })(value,null,result);
+    return result;
+}
+
+function writeObjectsToCSVStream(objects, stream) {
+    var keys = _.chain(objects)
+                .map(_.keys)
+                .flatten()
+                .uniq()
+                .value();
+    var idxs = {};
+    _.forEach(keys, function(key,idx){
+        idxs[key] = idx;
+    });
+    var writer = new CSV.CsvWriter(stream);
+    writer.writeRecord(keys);
+    _.forEach(objects, function(obj){
+        var row = [];
+        _.forEach(obj, function(val,key){
+            row[idxs[key]] = val;
+        });
+        writer.writeRecord(row);
+    });
+}
Index: Dev/trunk/src/server/config/config-couchdb.js
===================================================================
--- Dev/trunk/src/server/config/config-couchdb.js	(revision 474)
+++ Dev/trunk/src/server/config/config-couchdb.js	(revision 475)
@@ -1,53 +1,19 @@
-var q = require('q'),
-    request = require('../util/q-request'),
-    _ = require('underscore'),
-    util = require('util');
+var Q = require('q')
+  , request = require('../util/q-request')
+  , _ = require('underscore')
+  , CouchDB = require('../util/couch').CouchDB
+  , util = require('util');
 
 var designDocs = require('./couchdb-design-docs');
 
 module.exports = function(couchDbURL) {
-
-    function stringifyFunctions(value) {
-        if ( value === null ) {
-            return null;
-        } else if ( _.isArray(value) ) {
-            return value;
-        } else if ( _.isFunction(value) ) {
-            return value.toString();
-        } else if ( _.isObject(value) ) {
-            value = _.clone(value);
-            _.each(value, function(propValue, propName){
-                value[propName] = stringifyFunctions(propValue);
-            });
-            return value;
-        } else {
-            return value;
-        }
-    }
-
-    function dbRequest(method,path,content) {
-        var url = couchDbURL+path;
-        var options = {
-            method: method,
-            headers: {
-                'content-type': 'application/json; charset=utf-8',
-                'accept': 'application/json'
-            },
-            body: JSON.stringify(content)
-        };
-        //console.log('req',url,options);
-        return request(url,options).then(function(res){
-            return JSON.parse(res);
-        }, function(err){
-            return JSON.parse(err);
-        });
-    }
+    var server = new CouchDB(couchDbURL);
 
     console.log("Configuring CouchDB for QED");
     console.log("Checking CouchDB version");
-    return dbRequest('GET','')
-    .then(function(res){
-        if (res.version !== "1.2.0" ) {
-            console.log("Found "+res.version+", only tested with CouchDB 1.2.0");
+    return server.get('')
+    .then(function(info){
+        if (info.version !== "1.2.0" ) {
+            console.log("Found "+info.version+", only tested with CouchDB 1.2.0");
         } else {
             console.log("CouchDB 1.2.0 found");
@@ -55,15 +21,14 @@
     }).then(function(res){
         console.log("Checking database 'qed'");
-        return dbRequest('GET','qed')
-        .then(function(res){
+        return server.get('qed')
+        .then(function(db){
             console.log("Database 'qed' found.");
         },function(err){
             console.log("Creating database 'qed'");
-            return dbRequest('PUT','qed');
+            return server.put('qed');
         });
     }).then(function(){
         console.log("Putting documents in database.");
-        designDocs = stringifyFunctions(designDocs);
-        return q.all(_.map(designDocs, function(doc,docUrl){
+        return _.reduce(designDocs, function(memo, doc, docUrl) {
             var configAction = doc.__configAction || "replace";
             delete doc.__configAction;
@@ -71,30 +36,34 @@
             case "ignore":
                 console.log(docUrl+" ignored.");
-                break;
+                return memo;
             case "update":
                 console.log(docUrl+" updating.");
-                return dbRequest('GET',docUrl)
-                .then(function(oldDoc){
-                    _.extend(oldDoc,doc);
-                    return dbRequest('PUT',docUrl,oldDoc);
-                },function(){
-                    return dbRequest('PUT',docUrl,doc);
+                return memo.then(function(){
+                    server.get(docUrl)
+                    .then(function(oldDoc){
+                        _.extend(oldDoc,doc);
+                        return server.put(docUrl,oldDoc);
+                    },function(){
+                        return server.put(docUrl,doc);
+                    });
                 });
             case "replace":
                 console.log(docUrl+" replacing.");
-                return dbRequest('GET',docUrl)
-                .then(function(oldDoc){
-                    _.extend(doc,_.pick(oldDoc,'_id','_rev'));
-                    return dbRequest('PUT',docUrl,doc);
-                },function(){
-                    return dbRequest('PUT',docUrl,doc);
+                return memo.then(function(){
+                    server.get(docUrl)
+                    .then(function(oldDoc){
+                        _.extend(doc,_.pick(oldDoc,'_id','_rev'));
+                        return server.put(docUrl,doc);
+                    },function(){
+                        return server.put(docUrl,doc);
+                    });
                 });
             default:
                 console.warn("Unknown action",configAction);
-                break;
+                return memo;
             }
-        }));
+        }, Q.resolve());
     }).then(function(results){
-        console.log("Done!");
+        console.log("Done!", results);
     },function(err){
         console.error("ERROR",err,err.stack);
Index: Dev/trunk/src/server/heroku.js
===================================================================
--- Dev/trunk/src/server/heroku.js	(revision 474)
+++ Dev/trunk/src/server/heroku.js	(revision 475)
@@ -15,3 +15,5 @@
         console.log('Listening on port',port);
     });
+}, function(error){
+    console.error(error);
 });
Index: Dev/trunk/src/server/util/couch.coffee
===================================================================
--- Dev/trunk/src/server/util/couch.coffee	(revision 475)
+++ Dev/trunk/src/server/util/couch.coffee	(revision 475)
@@ -0,0 +1,46 @@
+request = require './q-request'
+_ = require 'underscore'
+Q = require 'q'
+
+class CouchDB
+    constructor: (url) ->
+        @url = normalizeURL url
+    get: (id) ->
+        url = "#{@url}/#{id}"
+        couchRequest 'GET', url
+    put: (id, doc) ->
+        url = "#{@url}/#{id}"
+        couchRequest 'PUT', url, doc
+
+normalizeURL = (url) ->
+    url.replace /\/$/, ''
+
+couchRequest = (method, url, data) ->
+    options =
+        method: method,
+        headers:
+            'content-type': 'application/json; charset=utf-8'
+            'accept': 'application/json'
+        body: JSON.stringify (stringifyFunctions (data || {}))
+    request(url, options)
+    .then (res) =>
+        JSON.parse res
+    , (err) =>
+        Q.reject (JSON.parse err)
+
+stringifyFunctions = (value) ->
+    if value is null
+        null
+    else if _.isArray value
+        value
+    else if _.isFunction value
+        value.toString()
+    else if _.isObject value
+        value = _.clone value
+        _.each value, (propValue, propName) =>
+            value[propName] = stringifyFunctions propValue
+        value
+    else
+        value
+
+exports.CouchDB = CouchDB
