Index: /Dev/trunk/src/client/qed-client/model/classes/_Class.js
===================================================================
--- /Dev/trunk/src/client/qed-client/model/classes/_Class.js	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/classes/_Class.js	(revision 525)
@@ -71,7 +71,9 @@
             } else if ( lang.isObject(obj) ) {
                 objectFuns.forEach(obj,function(v,prop){
-                    if ( v === null ||
-                         v === "" ||
-                         (typeof v === "number" && isNaN(v)) ) {
+                    if ( ( v === null ||
+                           v === "" ||
+                           (typeof v === "number" && isNaN(v)) ) && 
+                         prop !== 'subcode' ) // HACK : this hardcoded exclusion for subcode is quite nasty
+                    {
                         delete obj[prop];
                     } else {
Index: /Dev/trunk/src/client/qed-client/model/widgets/QuestionEditorPreviewItem.js
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/QuestionEditorPreviewItem.js	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/QuestionEditorPreviewItem.js	(revision 525)
@@ -149,6 +149,8 @@
             if ( this.innerWidget !== null ) {
                 if ( this._editing === true ) {
-                    this._onChange();
-                    // how to force event on widget here?
+                    // because the widget doesn't generate an event
+                    // unless it loses focus, we set the value here
+                    // just to be sure not to lose any user input
+                    this._setValueInternal(this.innerWidget.get('value'));
                 }
                 this.removeChild(this.innerWidget);
Index: /Dev/trunk/src/client/qed-client/model/widgets/questions/ScaleInputWidget.js
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/questions/ScaleInputWidget.js	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/questions/ScaleInputWidget.js	(revision 525)
@@ -22,5 +22,5 @@
             this.minNode.innerHTML = this.minLabel || "";
             this.maxNode.innerHTML = this.maxLabel || "";
-            if ( this.naLabel !== null ) {
+            if ( this.naLabel ) {
                 this.naNode.innerHTML = this.naLabel;
             }
@@ -56,5 +56,5 @@
                     className: 'max'
                 }, tr);
-                if ( this.naLabel !== null && this.naLabel !== "" ) {
+                if ( this.naLabel ) {
                     td = domConstruct.create("td", {}, tr);
                     radio = new RadioButton({
Index: /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/MultipleChoiceInputConfigWidget.html
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/MultipleChoiceInputConfigWidget.html	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/MultipleChoiceInputConfigWidget.html	(revision 525)
@@ -3,5 +3,5 @@
     <label class="qedLabel" for="subcode">Subcode</label>
     <input data-dojo-type="dijit/form/ValidationTextBox"
-           data-dojo-props="required:true,placeHolder:'Subcode'"
+           data-dojo-props="placeHolder:'Subcode'"
            class="subcode"
            name="subcode"/>
Index: /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/NumberInputConfigWidget.html
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/NumberInputConfigWidget.html	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/NumberInputConfigWidget.html	(revision 525)
@@ -5,5 +5,5 @@
          data-dojo-attach-point="subcodeBox"
          data-dojo-type="dijit/form/ValidationTextBox"
-         data-dojo-props="required: true, placeholder: 'Subcode'"
+         data-dojo-props="placeholder: 'Subcode'"
          name="subcode"></div>
   </div>
Index: /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/ScaleInputConfigRowWidget.html
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/ScaleInputConfigRowWidget.html	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/ScaleInputConfigRowWidget.html	(revision 525)
@@ -2,5 +2,5 @@
   <th class="subcode">
     <div data-dojo-type="dijit/form/ValidationTextBox"
-         data-dojo-props="required: true, placeholder: 'Subcode'"
+         data-dojo-props="placeholder: 'Subcode'"
          name="subcode"></div>
   </th>
Index: /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/StringInputConfigWidget.html
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/StringInputConfigWidget.html	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/StringInputConfigWidget.html	(revision 525)
@@ -5,5 +5,5 @@
          data-dojo-attach-point="subcodeBox"
          data-dojo-type="dijit/form/ValidationTextBox"
-         data-dojo-props="required: true, placeholder: 'Subcode'"
+         data-dojo-props="placeholder: 'Subcode'"
          name="subcode"></div>
   </div>
Index: /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/TextInputConfigWidget.html
===================================================================
--- /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/TextInputConfigWidget.html	(revision 524)
+++ /Dev/trunk/src/client/qed-client/model/widgets/questions/templates/TextInputConfigWidget.html	(revision 525)
@@ -5,5 +5,5 @@
          data-dojo-attach-point="subcodeBox"
          data-dojo-type="dijit/form/ValidationTextBox"
-         data-dojo-props="required: true, placeholder: 'Subcode'"
+         data-dojo-props="placeholder: 'Subcode'"
          name="subcode"></div>
   </div>
Index: /Dev/trunk/src/client/qed-client/widgets/ListWidget.js
===================================================================
--- /Dev/trunk/src/client/qed-client/widgets/ListWidget.js	(revision 524)
+++ /Dev/trunk/src/client/qed-client/widgets/ListWidget.js	(revision 525)
@@ -98,7 +98,13 @@
         _getValueAttr: function() {
             return array.map(
-                this._getDescendantFormWidgets(),
-                function(child) { return child.get('value'); }
-                ,this);
+                this.source.getAllNodes(),
+                lang.hitch(this,function(node){
+                    var widget = registry.byNode(node);
+                    if ( widget && 'value' in widget ) {
+                        return widget.get('value');
+                    } else {
+                        return this.source.getItem(node.id).data;
+                    }
+                }));
         },
         _setValueAttr: function(value,priorityChange) {
Index: /Dev/trunk/src/client/qed-client/widgets/_ComplexValueMixin.js
===================================================================
--- /Dev/trunk/src/client/qed-client/widgets/_ComplexValueMixin.js	(revision 524)
+++ /Dev/trunk/src/client/qed-client/widgets/_ComplexValueMixin.js	(revision 525)
@@ -64,5 +64,7 @@
                     if (typeof widgets[0].checked === 'boolean') {
                         array.forEach(widgets, function(w){
-                            w.set('value', array.indexOf(values, w._get('value')) !== -1, priorityChange);
+                            w.set('value',
+                                  array.indexOf(values,w._get('value'))!==-1,
+                                  priorityChange);
                         });
                     } else if (widgets[0].multiple) {
@@ -90,4 +92,5 @@
         _setDisabledAttr: function(disabled) {
             this.inherited(arguments);
+            this._set('disabled',disabled);
             array.forEach(
                 this._getDescendantFormWidgets(),
@@ -98,4 +101,5 @@
         _setReadOnlyAttr: function(readOnly) {
             this.inherited(arguments);
+            this._set('readOnly',readOnly);
             array.forEach(
                 this._getDescendantFormWidgets(),
Index: /Dev/trunk/src/package.json
===================================================================
--- /Dev/trunk/src/package.json	(revision 524)
+++ /Dev/trunk/src/package.json	(revision 525)
@@ -8,5 +8,4 @@
     "passport": "~0.1.18",
     "passport-local": "~0.1.6",
-    "q": "~0.9.7",
     "request": "~2.21.0",
     "ya-csv": "~0.9.3",
Index: /Dev/trunk/src/server/api/categories.js
===================================================================
--- /Dev/trunk/src/server/api/categories.js	(revision 525)
+++ /Dev/trunk/src/server/api/categories.js	(revision 525)
@@ -0,0 +1,42 @@
+var express = require("express")
+  , HTTPResult = require("../util/http-result")
+  , _ = require("underscore")
+  ;
+
+module.exports = function(couch,schema) {
+    var util = require('./util')(couch,schema);
+    var topics = require('./topics')(couch,schema);
+    var exports = {};
+
+    var app = exports.app = express();
+
+    var getCategories = exports.getCategories = function(sub) {
+        var url = '_design/questions/_view/'+(sub||'all');
+        return couch.get(url,{query:{reduce:true,group:true,group_level:1}})
+        .handle({
+            200: function(result) {
+                return _.map(result.rows, function(item) {
+                    return { name:item.key[0], count:item.value };
+                });
+            },
+            default: util.handleUnknownResponse
+        });
+    };
+
+    app.get('/',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var sub = 'published' in req.query && 'published';
+            getCategories(sub)
+            .handle(res.send.bind(res));
+        });
+    app.get('/:category/topics',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var category = req.params.category;
+            var sub = 'published' in req.query && 'published';
+            topics.getTopicsWithCategory(category,sub)
+            .handle(res.send.bind(res));
+        });
+    return exports;
+};
Index: /Dev/trunk/src/server/api/open.js
===================================================================
--- /Dev/trunk/src/server/api/open.js	(revision 525)
+++ /Dev/trunk/src/server/api/open.js	(revision 525)
@@ -0,0 +1,147 @@
+var express = require("express")
+  , cryptoken = require("../util/crypto-token")
+  , HTTPResult = require("../util/http-result")
+  , etags = require("../util/etags")
+  ;
+
+module.exports = function(couch,schema) {
+    var exports = {};
+
+    var util = require('./util')(couch,schema);
+    var app = exports.app = express();
+
+    var isSurveyRunRunning = exports.isSurveyRunRunning = function(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('/responses/:id',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var id = req.params.id;
+            var rev = etags.parse(req.header('If-Non-Match'))[0];
+            var secret = req.query.secret;
+            util.getDocument(id,rev,'Response')
+            .handle({
+                200: function(response) {
+                    if ( response.secret === secret ) {
+                        return util.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('/responses',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var secret = req.query.secret;
+            var response = req.body;
+            delete response._surveyRun;
+            util.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 cryptoken()
+                        .then(function(token) {
+                            response.secret = token;
+                            return util.postDocument('Response',response)
+                            .then(function(doc){
+                                doc._surveyRun = surveyRun;
+                                return doc;
+                            });
+                        });
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.put('/responses/:id',
+        util.ensureMIME(util.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;
+            util.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 util.getDocument(doc.surveyRunId,[],'SurveyRun')
+                        .handle({
+                            200: function(surveyRun) {
+                                if ( !isSurveyRunRunning(surveyRun) ) {
+                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
+                                } else {
+                                    return util.putDocument(id,rev,'Response',doc)
+                                    .handle({
+                                        200: function(doc) {
+                                            doc._surveyRun = surveyRun;
+                                            return new HTTPResult(201,doc);
+                                        }
+                                    });
+                                }
+                            }
+                        });
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+    app.delete('/responses/:id',
+        util.ensureMIME(util.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;
+            util.getDocument(id,[],'Response')
+            .handle({
+                200: function(prev) {
+                    if ( prev.secret !== secret ) {
+                        return new HTTPResult(403,{error: "Secrets are not the same."});
+                    } else {
+                        return util.getDocument(doc.surveyRunId,[],'SurveyRun')
+                        .handle({
+                            200: function(surveyRun) {
+                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
+                                    return util.deleteDocument(id,rev,doc);
+                                } else {
+                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
+                                }
+                            }
+                        });
+                    }
+                }
+            })
+            .handle(res.send.bind(res));
+        });
+
+    return exports;
+};
Index: /Dev/trunk/src/server/api/questions.js
===================================================================
--- /Dev/trunk/src/server/api/questions.js	(revision 525)
+++ /Dev/trunk/src/server/api/questions.js	(revision 525)
@@ -0,0 +1,169 @@
+var express = require("express")
+  , HTTPResult = require("../util/http-result")
+  , _ = require("underscore")
+  , objF = require("../util/object")
+  , etags = require("../util/etags")
+  ;
+
+module.exports = function(couch,schema) {
+    var util = require('./util')(couch,schema);
+    var exports = {};
+
+    var app = exports.app = express();
+
+    var getQuestionsWithCode = exports.getQuestionsWithCode = function(code,sub) {
+        var url = '_design/questions/_view/'+(sub||'all')+'_by_code';
+        var query = {reduce:false,include_docs:true,key:code};
+        return couch.get(url,{query:query})
+        .handle({
+            200: util.handleRowDocs,
+            default: util.handleUnknownResponse
+        });
+    };
+    var getQuestions = exports.getQuestions = function(sub) {
+        var url = '_design/questions/_view/'+(sub||'all');
+        var query = {reduce:false,include_docs:true};
+        return couch.get(url,{query:query})
+        .handle({
+            200: util.handleRowDocs,
+            default: util.handleUnknownResponse
+        });
+    };
+    var getQuestionsAndCodes = exports.getQuestionsAndCodes = function(sub) {
+        var url = '_design/questions/_view/'+(sub||'all')+'_by_code';
+        var query = {};
+        return couch.get(url,{query:query})
+        .handle({
+            200: util.handleRowDictOfValues,
+            default: util.handleUnknownResponse
+        });
+    };
+    var getQuestionsWithTopic = exports.getQuestionsWithTopic = function(topic,sub) {
+        var url = '_design/questions/_view/'+(sub||'all')+'_topics';
+        var query = {reduce:false,include_docs:true,key:topic};
+        return couch.get(url,{query:query})
+        .handle({
+            200: util.handleRowDocs,
+            default: util.handleUnknownResponse
+        });
+    };
+    var getQuestionsWithCategoryAndTopic = exports.getQuestionsWithCategoryAndTopic = function(category,topic,sub) {
+        var hasTopic = typeof topic !== 'undefined';
+        var url = '_design/questions/_view/'+(sub||'all');
+        var query = {reduce:false,include_docs:true,
+                     startkey:hasTopic?[category,topic]:[category],
+                     endkey:hasTopic?[category,topic]:[category,{}]};
+        return couch.get(url,{query:query})
+        .handle({
+            200: util.handleRowDocs,
+            default: util.handleUnknownResponse
+        });
+    };
+    var areElementsUnique = exports.areElementsUnique = function(array) {
+        return _.uniq(array).length === array.length;
+    };
+    var getVariablesAndIds = exports.getVariablesAndIds = function(sub) {
+        var url = '_design/questions/_view/'+(sub||'all')+'_variables';
+        return couch.get(url)
+        .handle({
+            200: util.handleRowDictOfValues,
+            default: util.handleUnknownResponse
+        });
+    };
+    var collectVariables = exports.collectVariables = function(question) {
+        return _.chain(objF.collectFields('subcode',question.content))
+        .map(function(subcode){
+            return question.code+subcode;
+        }).value();
+    };
+
+    app.get('/',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var sub = 'published' in req.query && 'published';
+            var hr;
+            if ( 'category' in req.query ) {
+                hr = getQuestionsWithCategoryAndTopic(req.query.category,
+                                                      req.query.topic,sub);
+            } else if ( 'topic' in req.query ) {
+                hr = getQuestionsWithTopic(req.query.topic,sub);
+            } else if ( 'code' in req.query ) {
+                hr = getQuestionsWithCode(req.query.code,sub);
+            } else {
+                hr = getQuestions(sub);
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.post('/',
+        util.ensureMIME(util.JSON_MIME),
+        function (req,res) {
+            var doc = req.body;
+            var ourVars = collectVariables(doc);
+            var hr;
+            var dups = objF.findDuplicates(ourVars);
+            if ( dups.length > 0 ) {
+                hr = new HTTPResult(400,{error:"Question defines variable(s) "+
+                                               dups.join(',')+
+                                               " multiple times (subcodes not unique)."});
+            } else {
+                hr = getVariablesAndIds()
+                .handle({
+                    200: function(vars) {
+                        vars = _.chain(vars).keys().uniq().value(); // don't punish us for others mistakes
+                        dups = objF.findDuplicates(vars.concat(ourVars));
+                        if ( dups.length > 0 ) {
+                            return new HTTPResult(400,{error:"Question redefines variable(s) "+
+                                                             dups.join(',')+
+                                                             " (already defined in other question(s))."});
+                        } else {
+                            return util.postDocument('Question',doc);
+                        }
+                    }
+                });
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.get('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocGet_id('Question'));
+    app.put('/:id',
+        util.ensureMIME(util.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 ourVars = collectVariables(doc);
+            var hr;
+            var dups = objF.findDuplicates(ourVars);
+            if ( dups.length > 0 ) {
+                hr = new HTTPResult(400,{error:"Question defines variable(s) "+
+                                               dups.join(',')+
+                                               " multiple times (subcodes not unique)."});
+            } else {
+                hr = getVariablesAndIds()
+                .handle({
+                    200: function(vars) {
+                        vars = _.chain(vars).map(function(otherId,otherVar){
+                            return otherId !== id ? otherVar : null;
+                        }).filter(function(otherCode){
+                            return otherCode !== null;
+                        }).uniq().value(); // don't punish us for others mistakes
+                        dups = objF.findDuplicates(vars.concat(ourVars));
+                        if ( dups.length > 0 ) {
+                            return new HTTPResult(400,{error:"Question redefines variable(s) "+
+                                                             dups.join(',')+
+                                                             " (already defined in other question(s))."});
+                        } else {
+                            return util.putDocument(id,rev,'Question',doc);
+                        }
+                    }
+                });
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.delete('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocDel_id('Question'));
+
+    return exports;
+};
Index: /Dev/trunk/src/server/api/responses.js
===================================================================
--- /Dev/trunk/src/server/api/responses.js	(revision 525)
+++ /Dev/trunk/src/server/api/responses.js	(revision 525)
@@ -0,0 +1,72 @@
+var express = require("express")
+  , HTTPResult = require("../util/http-result")
+  , etags = require("../util/etags")
+  , cryptoken = require("../util/crypto-token")
+  , _ = require('underscore')
+  ;
+
+module.exports = function(couch,schema) {
+    var exports = {};
+
+    var util = require('./util')(couch,schema);
+    var app = exports.app = express();
+
+    var getResponsesBySurveyRunId = exports.getResponsesBySurveyRunId = function(surveyRunId) {
+        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
+        return couch.get(url)
+        .handle({
+            200: util.handleRowValues,
+            default: util.handleUnknownResponse
+        });
+    };
+
+    app.get('/',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res){
+            var hr;
+            if ( 'surveyRunId' in req.query ) {
+                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
+            } else {
+                hr = util.getDocumentsOfType('Response');
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.get('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocGet_id('Response'));
+    app.post('/api/responses',
+        util.ensureMIME(util.JSON_MIME),
+        function (req,res) {
+            var doc = req.body;
+            cryptoken()
+            .then(function(token) {
+                doc.secret = token;
+                return util.postDocument('Response',doc);
+            })
+            .handle(res.send.bind(res));
+        });
+    app.put('/:id',
+        util.ensureMIME(util.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 = util.randomToken()
+                .handle({
+                    201: function(token) {
+                        doc.secret = token;
+                        return util.putDocument(id,rev,'Response',doc);
+                    }
+                });
+            } else {
+                hr = util.putDocument(id,rev,'Response',doc);
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.delete('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocDel_id('Response'));
+    return exports;
+};
Index: /Dev/trunk/src/server/api/surveyRuns.js
===================================================================
--- /Dev/trunk/src/server/api/surveyRuns.js	(revision 525)
+++ /Dev/trunk/src/server/api/surveyRuns.js	(revision 525)
@@ -0,0 +1,102 @@
+var express = require("express")
+  , HTTPResult = require("../util/http-result")
+  , cryptoken = require("../util/crypto-token")
+  , etags = require("../util/etags")
+  , _ = require('underscore')
+  ;
+
+module.exports = function(couch,schema) {
+    var exports = {};
+
+    var util = require('./util')(couch,schema);
+    var responses = require('./responses')(couch,schema);
+    var app = exports.app = express();
+
+    app.get('/',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocsGet('SurveyRun'));
+    app.post('/',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var doc = req.body;
+            var hr;
+            if ( !util.isDocPublished(doc.survey) ) {
+                hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
+            } else {
+                hr = cryptoken()
+                .then(function(token) {
+                    doc.secret = token;
+                    return util.postDocument('SurveyRun',doc);
+                });
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.get('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocGet_id('SurveyRun'));
+    app.put('/:id',
+        util.ensureMIME(util.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 ( !util.isDocPublished(doc.survey) ) {
+                hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
+            } else if ( typeof doc.secret === 'undefined' ) {
+                hr = cryptoken()
+                .then(function(token) {
+                    doc.secret = token;
+                    return util.putDocument(id,rev,'SurveyRun',doc);
+                });
+            } else {
+                hr = util.putDocument(id,rev,'SurveyRun',doc);
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.delete('/:id',
+        util.ensureMIME(util.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);
+            responses.getResponsesBySurveyRunId(id)
+            .handle({
+                200: function(responses) {
+                    if ( responses.length > 0 ) {
+                        return new HTTPResult(403,{error:"Cannot delete run that has responses."});
+                    } else {
+                        return util.deleteDocument(id,rev);
+                    }
+                }
+            }).handle(res.send.bind(res));
+        });
+    app.get('/:id/responses',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var id = req.params.id;
+            responses.getResponsesBySurveyRunId(id)
+            .handle(res.send.bind(res));
+        });
+    app.get('/:id/responses.csv',
+        util.ensureMIME(util.CSV_MIME),
+        function(req, res) {
+            var id = req.params.id;
+            responses.getResponsesBySurveyRunId(id)
+            .handle({
+                200: function(responses) {
+                    var answers = _.map(responses,function(response){
+                        return response.answers;
+                    });
+                    res.set({
+                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
+                    });
+                    res.status(200);
+                    util.writeObjectsToCSVStream(answers, res);
+                    res.end();
+                },
+                default: res.send.bind(res)
+            });
+        });
+    return exports;
+};
Index: /Dev/trunk/src/server/api/surveys.js
===================================================================
--- /Dev/trunk/src/server/api/surveys.js	(revision 525)
+++ /Dev/trunk/src/server/api/surveys.js	(revision 525)
@@ -0,0 +1,81 @@
+var express = require("express")
+  , _ = require("underscore")
+  , HTTPResult = require("../util/http-result")
+  , etags = require("../util/etags")
+  ;
+
+module.exports = function(couch,schema) {
+    var exports = {};
+    var util = require('./util')(couch,schema);
+    var app = exports.app = express();
+
+    app.get('/',
+        util.ensureMIME(util.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');
+            }
+            couch.get(url)
+            .handle({
+                200: function(result) {
+                    return _.map(result.rows, function(item) {
+                        return item.value;
+                    });
+                },
+                default: util.handleUnknownResponse
+            })
+            .handle(res.send.bind(res));
+        });
+    app.post('/',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var doc = req.body;
+            var hr;
+            if ( !util.areDocsUnique(doc.questions) ) {
+                hr = new HTTPResult(400,{error:"Survey contains duplicate questions."});
+            } else if ( util.isDocPublished(doc) &&
+                        !util.areDocsPublished(doc.questions) ) {
+                hr = new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."});
+            } else {
+                hr = util.postDocument('Survey',doc);
+            }
+            hr.handle(res.send.bind(res));
+        });
+    app.get('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocGet_id('Survey'));
+    app.put('/:id',
+        util.ensureMIME(util.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);
+            if ( !util.areDocsUnique(doc.questions) ) {
+                new HTTPResult(400,{error:"Survey contains duplicate questions."})
+                .handle(res.send.bind(res));
+            } else if ( util.isDocPublished(doc) &&
+                        !util.areDocsPublished(doc.questions) ) {
+                new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."})
+                .handle(res.send.bind(res));
+            } else {
+                util.putDocument(id,rev,'Survey',doc)
+                .handle({
+                    201: function(doc) {
+                        res.set({
+                            'ETag': etags.format([doc._rev])
+                        }).send(201, doc);
+                    },
+                    default: res.send.bind(res)
+                });
+            }
+        });
+    app.delete('/:id',
+        util.ensureMIME(util.JSON_MIME),
+        util.makeDocDel_id('Survey'));
+    return exports;
+};
Index: /Dev/trunk/src/server/api/topics.js
===================================================================
--- /Dev/trunk/src/server/api/topics.js	(revision 525)
+++ /Dev/trunk/src/server/api/topics.js	(revision 525)
@@ -0,0 +1,47 @@
+var express = require("express")
+  , HTTPResult = require("../util/http-result")
+  , _ = require("underscore")
+  ;
+
+module.exports = function(couch,schema) {
+    var exports = {};
+    
+    var util = require('./util')(couch,schema);
+    var app = exports.app = express();
+
+    var getTopics = exports.getTopics = function(sub) {
+        var url = '_design/questions/_view/'+(sub||'all')+'_topics';
+        return couch.get(url,{query:{reduce:true,group:true}})
+        .handle({
+            200: function(result) {
+                return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
+            },
+            default: util.handleUnknownResponse
+        });
+    };
+    var getTopicsWithCategory = exports.getTopicsWithCategory = function(category,sub) {
+        var url = '_design/questions/_view/'+(sub||'all');
+        return couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}})
+        .handle({
+            200: function(result) {
+                return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
+            },
+            default: util.handleUnknownResponse
+        });
+    };
+
+    app.get('/',
+        util.ensureMIME(util.JSON_MIME),
+        function(req,res) {
+            var sub = 'published' in req.query && 'published';
+            var hr;
+            if ( 'category' in req.query ) {
+                hr = getTopicsWithCategory(req.query.category,sub);
+            } else {
+                hr = getTopics(sub);
+            }
+            hr.handle(res.send.bind(res));
+        });
+
+    return exports;
+};
Index: /Dev/trunk/src/server/api/util.js
===================================================================
--- /Dev/trunk/src/server/api/util.js	(revision 525)
+++ /Dev/trunk/src/server/api/util.js	(revision 525)
@@ -0,0 +1,266 @@
+var _ = require('underscore')
+  , HTTPResult = require("../util/http-result")
+  , validator = require("../util/validator")
+  , etags = require("../util/etags")
+  , cryptoken = require('../util/crypto-token')
+  , CSV = require("ya-csv")
+  ;
+
+module.exports = function(couch,schema) {
+    var exports = {};
+
+    var identity = exports.identity = function(obj) { return obj; };
+    var stripAndReturnPrivates = exports.stripAndReturnPrivates = function(obj) {
+        var priv = {};
+        _.each(obj||{},function(val,key){
+            if (key.substring(0,1) === '_') {
+                priv[key] = val;
+                delete obj[key];
+            }
+        });
+        return priv;
+    };
+    var areDocsUnique = exports.areDocsUnique = function(docs) {
+        return _.chain(docs)
+                .map(function(doc){ return doc._id; })
+                .uniq()
+                .value().length === docs.length;
+    };
+    var isDocPublished = exports.isDocPublished = function(doc) {
+        return !!doc.publicationDate;
+    };
+    var areDocsPublished = exports.areDocsPublished = function(docs) {
+        return _.every(docs, isDocPublished);
+    };
+
+    var handleUnknownResponse = exports.handleUnknownResponse = function(status,error) {
+        return new HTTPResult(500,{error: error.reason});
+    };
+    var handleRowKeys = exports.handleRowKeys = function(result) {
+        return _.map(result.rows, function(item) { return item.key; });
+    };
+    var handleRowValues = exports.handleRowValues = function(result) {
+        return _.map(result.rows, function(item) { return item.value; });
+    };
+    var handleRowDocs = exports.handleRowDocs = function(result) {
+        return _.map(result.rows, function(item) { return item.doc; });
+    };
+    var handleRowFirstDoc = exports.handleRowFirstDoc = function(result) {
+        if ( result.rows.length > 0 ) {
+            return result.rows[0].doc;
+        } else {
+            return new HTTPResult(404,{error:"No document found."});
+        }
+    };
+    var handleRowFirstValue = exports.handleRowFirstValue = function(result) {
+        if ( result.rows.length > 0 ) {
+            return result.rows[0].value;
+        } else {
+            return new HTTPResult(404,{error:"No document found."});
+        }
+    };
+    var handleRowDictOfDocs = exports.handleRowDictOfDocs = function(result) {
+        return _.reduce(result.rows, function(dict,item) {
+            dict[item.key] = item.doc;
+            return dict;
+        }, {});
+    };
+    var handleRowDictOfValues = exports.handleRowDictOfValues = function(result) {
+        return _.reduce(result.rows, function(dict,item) {
+            dict[item.key] = item.value;
+            return dict;
+        }, {});
+    };
+
+    var getDocumentsOfType = exports.getDocumentsOfType = function(type) {
+        var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
+        return couch.get(url)
+        .handle({
+            200: handleRowValues,
+            404: function() { return {error: "Cannot find collection of type "+type}; },
+            default: handleUnknownResponse
+        });
+    };
+    var getDocument = exports.getDocument = function(id,rev,type) {
+        var opts = {headers:{}};
+        if (rev) {
+            opts.headers['If-Non-Match'] = '"'+rev+'"';
+        }
+        return couch.get(id,opts)
+        .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
+        });
+    };
+    var putDocument = exports.putDocument = function(id,rev,type,doc) {
+        var priv = stripAndReturnPrivates(doc);
+        var valid;
+        if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
+            var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
+            return couch.put(id,doc,opts)
+            .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.", valid: valid});
+        }
+    };
+    var deleteDocument = exports.deleteDocument = function(id,rev) {
+        if ( rev ) {
+            var opts = {headers:{'If-Match':'"'+rev+'"'}};
+            return couch.delete(id,opts)
+            .handle({
+                200: identity,
+                409: function(error) {
+                    return {error: error.reason};
+                },
+                default: handleUnknownResponse
+            });
+        } else {
+            return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
+        }
+    };
+    var postDocument = exports.postDocument = function(type,doc) {
+        var priv = stripAndReturnPrivates(doc);
+        var valid;
+        if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
+            return couch.post(doc)
+            .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.", valid: valid});
+        }
+    };
+
+    var makeDocsGet = exports.makeDocsGet = function(type) {
+        return function(req,res) {
+            getDocumentsOfType(type)
+            .handle(res.send.bind(res));
+        };
+    };
+    var makeDocGet_id = exports.makeDocGet_id = function(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)
+            });
+        };
+    };
+    var makeDocPut_id = exports.makeDocPut_id = function(type) {
+        return function(req,res) {
+            var id = req.params.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)
+            });
+        };
+    };
+    var makeDocDel_id = exports.makeDocDel_id = function(type) {
+        return function(req,res) {
+            var id = req.params.id;
+            var doc = req.body;
+            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
+            deleteDocument(id,rev)
+            .handle(res.send.bind(res));
+        };
+    };
+    var makeDocPost = exports.makeDocPost = function(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)
+            });
+        };
+    };
+
+    var JSON_MIME = exports.JSON_MIME = 'application/json';
+    var CSV_MIME = exports.CSV_MIME = 'application/json';
+    var ensureMIME = exports.ensureMIME = function(mimeType) {
+        return function(req,res,next) {
+            if (!req.accepts(mimeType)) {
+                res.send(406);
+            } else {
+                res.set({
+                    'Content-Type': mimeType
+                });
+                next();
+            }
+        };
+    };
+
+    var writeObjectsToCSVStream = exports.writeObjectsToCSVStream = function(objects, stream, prelude) {
+        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);
+        if ( prelude ) {
+            _.forEach(prelude, function(val,key){
+                writer.writeRecord([key,val]);
+            });
+        }
+        writer.writeRecord(keys);
+        _.forEach(objects, function(obj){
+            var row = [];
+            _.forEach(obj, function(val,key){
+                row[idxs[key]] = val;
+            });
+            writer.writeRecord(row);
+        });
+    };
+
+    return exports;
+};
Index: /Dev/trunk/src/server/app.js
===================================================================
--- /Dev/trunk/src/server/app.js	(revision 524)
+++ /Dev/trunk/src/server/app.js	(revision 525)
@@ -5,15 +5,10 @@
   , path = require("path")
   , proxy = require("./util/simple-http-proxy")
-  , CSV = require("ya-csv")
-  , CouchDB = require('./util/couch').CouchDB
+  , CouchDB = require('./util/couch')
+  , HTTPResult = require('./util/http-result')
   , _ = require("underscore")
-  , validator = require("./util/validator")
-  , HTTPResult = require("./util/http-result")
-  , etags = require("./util/etags")
-  , cryptoken = require('./util/crypto-token')
-  , Q = require('q')
   ;
 
-exports.App = function(env) {
+exports.app = function(env) {
 
     assertSetting("couchServerURL", env, _.isString);
@@ -27,9 +22,11 @@
     .then(function(schemaInfo){
         if ( schemaInfo.version !== schema.version ) {
-            return Q.reject(new Error("Found schema version "+schemaInfo.version+
-                                      ", expected "+schema.version));
+            return HTTPResult.fail(
+                new Error("Found schema version "+schemaInfo.version+
+                          ", expected "+schema.version));
         } else if ( schemaInfo.viewsVersion !== views.schemaInfo.viewsVersion ) {
-            return Q.reject(new Error("Found views version "+schemaInfo.viewsVersion+
-                                      ", expected "+views.schemaInfo.viewsVersion));
+            return HTTPResult.fail(
+                new Error("Found views version "+schemaInfo.viewsVersion+
+                          ", expected "+views.schemaInfo.viewsVersion));
         } else {
             return configureApp(env,couch,schema);
@@ -40,4 +37,6 @@
 
 function configureApp(env,couch,schema) {
+
+    var apiUtil = require('./api/util')(couch,schema);
 
     function clientPath(relativePath) {
@@ -91,9 +90,9 @@
 
     // static resources
-    app.get('/', function(request, response){
-        response.sendfile(clientPath('index.html'));
+    app.get('/', function(req, res){
+        res.sendfile(clientPath('index.html'));
     });
-    app.get('/*.html', function(request, response) {
-        response.sendfile(clientPath(request.path));
+    app.get('/*.html', function(req, res) {
+        res.sendfile(clientPath(req.path));
     });
     _.each(['/dojo', '/dijit', '/dojox', '/qed', '/qed-client'], function(dir){
@@ -122,837 +121,33 @@
         });
 
-    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 identity(obj) { return obj; }
-    function stripAndReturnPrivates(obj) {
-        var priv = {};
-        _.each(obj||{},function(val,key){
-            if (key.substring(0,1) === '_') {
-                priv[key] = val;
-                delete obj[key];
-            }
-        });
-        return priv;
-    }
-    function areDocsUnique(docs) {
-        return _.chain(docs)
-                .map(function(doc){ return doc._id; })
-                .uniq()
-                .value().length === docs.length;
-    }
-    function areDocsPublished(docs) {
-        return _.every(docs, isDocPublished);
-    }
-    function isDocPublished(doc) {
-        return doc.publicationDate;
-    }
+    app.use('/api/questions', ensureAuthenticated);
+    app.use('/api/questions',
+            require('./api/questions')(couch,schema).app);
 
-    function handleUnknownResponse(status,error) {
-        return new HTTPResult(500,{error: error.reason});
-    }
-    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 handleRowFirstDoc(result) {
-        if ( result.rows.length > 0 ) {
-            return result.rows[0].doc;
-        } else {
-            return new HTTPResult(404,{error:"No document found."});
-        }
-    }
-    function handleRowFirstValue(result) {
-        if ( result.rows.length > 0 ) {
-            return result.rows[0].value;
-        } else {
-            return new HTTPResult(404,{error:"No document found."});
-        }
-    }
-    function handleRowDictOfDocs(result) {
-        return _.reduce(result.rows, function(dict,item) {
-            dict[item.key] = item.doc;
-            return dict;
-        }, {});
-    }
-    function handleRowDictOfValues(result) {
-        return _.reduce(result.rows, function(dict,item) {
-            dict[item.key] = item.value;
-            return dict;
-        }, {});
-    }
+    app.use('/api/categories', ensureAuthenticated);
+    app.use('/api/categories',
+            require('./api/categories')(couch,schema).app);
 
-    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);
-        var valid;
-        if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
-            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.", valid: valid});
-        }
-    }
-    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);
-        var valid;
-        if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
-            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.", valid: valid});
-        }
-    }
+    app.use('/api/topics', ensureAuthenticated);
+    app.use('/api/topics',
+            require('./api/topics')(couch,schema).app);
 
-    function makeDocsGet(type) {
-        return function(req,res) {
-            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 makeDocPut_id(type) {
-        return function(req,res) {
-            var id = req.params.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 makeDocDel_id(type) {
-        return function(req,res) {
-            var id = req.params.id;
-            var doc = req.body;
-            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)
-            });
-        };
-    }
+    app.use('/api/surveys', ensureAuthenticated);
+    app.use('/api/surveys',
+            require('./api/surveys')(couch,schema).app);
 
-    // Questions
-    function getQuestionsWithCode(code,sub) {
-        var url = '_design/questions/_view/'+(sub||'all')+'_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 getQuestions(sub) {
-        var url = '_design/questions/_view/'+(sub||'all');
-        var query = {include_docs:true};
-        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
-                                              handleUnknownError)
-        .handle({
-            200: handleRowDocs,
-            default: handleUnknownResponse
-        });
-    }
-    function getQuestionsAndCodes() {
-        var url = '_design/questions/_view/by_code';
-        var query = {include_docs:true};
-        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
-                                              handleUnknownError)
-        .handle({
-            200: handleRowDictOfDocs,
-            default: handleUnknownResponse
-        });
-    }
-    function getQuestionsWithTopic(topic,sub) {
-        var url = '_design/questions/_view/'+(sub||'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,sub) {
-        var hasTopic = typeof topic !== 'undefined';
-        var url = '_design/questions/_view/'+(sub||'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
-        });
-    }
-    function collectFields(field,obj) {
-        if ( _.isArray(obj) ) {
-            return _.reduce(obj,function(subcodes,val,idx){
-                return subcodes.concat(collectFields(field,val));
-            },[]);
-        } else if ( _.isObject(obj) ) {
-            return _.reduce(obj,function(subcodes,val,key){
-                if ( key === field ) {
-                    subcodes.push(val);
-                }
-                return subcodes.concat(collectFields(field,val));
-            },[]);
-        } else {
-            return [];
-        }
-    }
-    function areSubcodesUnique(content) {
-        var subcodes = collectFields('subcode',content);
-        return _.uniq(subcodes).length === subcodes.length;
-    }
-    app.get('/api/questions',
-        ensureAuthenticated,
-        ensureMIME(JSON_MIME),
-        function(req,res) {
-            var sub = 'published' in req.query && 'published';
-            var hr;
-            if ( 'category' in req.query ) {
-                hr = getQuestionsWithCategoryAndTopic(req.query.category,
-                                                      req.query.topic,sub);
-            } else if ( 'topic' in req.query ) {
-                hr = getQuestionsWithTopic(req.query.topic,sub);
-            } else if ( 'code' in req.query ) {
-                hr = getQuestionsWithCode(req.query.code,sub);
-            } else {
-                hr = getQuestions(sub);
-            }
-            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 if ( !areSubcodesUnique(doc.content) ) {
-                        return new HTTPResult(400,{error:"Subcodes are not unqiue."});
-                    } 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 && _.some(others,function(other){ return other._id !== id; })  ) {
-                        return new HTTPResult(403,{error:"Other question with this code already exists."});
-                    } else if ( !areSubcodesUnique(doc.content) ) {
-                        return new HTTPResult(400,{error:"Subcodes are not unqiue."});
-                    } else {
-                        return putDocument(id,rev,'Question',doc);
-                    }
-                }
-            })
-            .handle(res.send.bind(res));
-        });
-    app.delete('/api/questions/:id',
-        ensureAuthenticated,
-        ensureMIME(JSON_MIME),
-        makeDocDel_id('Question'));
+    app.use('/api/surveyRuns', ensureAuthenticated);
+    app.use('/api/surveyRuns',
+            require('./api/surveyRuns')(couch,schema).app);
 
-    // Categories and topics
-    function getTopics(sub) {
-        var url = '_design/questions/_view/'+(sub||'all')+'_topics';
-        return 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
-        });
-    }
-    function getCategories(sub) {
-        var url = '_design/questions/_view/'+(sub||'all');
-        return 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
-        });
-    }
-    function getTopicsWithCategory(category,sub) {
-        var url = '_design/questions/_view/'+(sub||'all');
-        return 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
-        });
-    }
-    app.get('/api/categories',
-        ensureAuthenticated,
-        ensureMIME(JSON_MIME),
-        function(req,res) {
-            var sub = 'published' in req.query && 'published';
-            getCategories(sub)
-            .handle(res.send.bind(res));
-        });
-    app.get('/api/categories/:category/topics',
-        ensureAuthenticated,
-        ensureMIME(JSON_MIME),
-        function(req,res) {
-            var category = req.params.category;
-            var sub = 'published' in req.query && 'published';
-            getTopicsWithCategory(category,sub)
-            .handle(res.send.bind(res));
-        });
-    app.get('/api/topics',
-        ensureAuthenticated,
-        ensureMIME(JSON_MIME),
-        function(req,res) {
-            var sub = 'published' in req.query && 'published';
-            var hr;
-            if ( 'category' in req.query ) {
-                hr = getTopicsWithCategory(req.query.category,sub);
-            } else {
-                hr = getTopics(sub);
-            }
-            hr.handle(res.send.bind(res));
-        });
+    app.use('/api/responses', ensureAuthenticated);
+    app.use('/api/responses',
+            require('./api/responses')(couch,schema).app);
 
-    // 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),
-        function(req,res) {
-            var doc = req.body;
-            var hr;
-            if ( !areDocsUnique(doc.questions) ) {
-                hr = new HTTPResult(400,{error:"Survey contains duplicate questions."});
-            } else if ( isDocPublished(doc) && !areDocsPublished(doc.questions) ) {
-                hr = new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."});
-            } else {
-                hr = postDocument('Survey',doc);
-            }
-            hr.handle(res.send.bind(res));
-        });
-    app.get('/api/surveys/:id',
-        ensureAuthenticated,
-        ensureMIME(JSON_MIME),
-        makeDocGet_id('Survey'));
-    app.put('/api/surveys/: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);
-            if ( !areDocsUnique(doc.questions) ) {
-                new HTTPResult(400,{error:"Survey contains duplicate questions."})
-                .handle(res.send.bind(res));
-            } else if ( isDocPublished(doc) && !areDocsPublished(doc.questions) ) {
-                new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."})
-                .handle(res.send.bind(res));
-            } else {
-                putDocument(id,rev,'Survey',doc)
-                .handle({
-                    201: function(doc) {
-                        res.set({
-                            'ETag': etags.format([doc._rev])
-                        }).send(201, doc);
-                    },
-                    default: res.send.bind(res)
-                });
-            }
-        });
-    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;
-            var hr;
-            if ( !isDocPublished(doc.survey) ) {
-                hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
-            } else {
-                hr = randomToken()
-                .handle({
-                    201: function(token) {
-                        doc.secret = token;
-                        return postDocument('SurveyRun',doc);
-                    }
-                });
-            }
-            hr.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 ( !isDocPublished(doc.survey) ) {
-                hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
-            } else if ( typeof doc.secret === 'undefined' ) {
-                hr = randomToken()
-                .handle({
-                    201: function(token) {
-                        doc.secret = token;
-                        return putDocument(id,rev,'SurveyRun',doc);
-                    }
-                });
-            } else {
-                hr = putDocument(id,rev,'SurveyRun',doc);
-            }
-            hr.handle(res.send.bind(res));
-        });
-    app.delete('/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);
-            getResponsesBySurveyRunId(id)
-            .handle({
-                200: function(responses) {
-                    if ( responses.length > 0 ) {
-                        return new HTTPResult(403,{error:"Cannot delete run that has responses."});
-                    } else {
-                        return deleteDocument(id,rev);
-                    }
-                }
-            }).handle(res.send.bind(res));
-        });
-    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 answers = _.map(responses,function(response){
-                        return response.answers;
-                    });
-                    res.set({
-                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
-                    });
-                    res.status(200);
-                    writeObjectsToCSVStream(answers, res);
-                    res.end();
-                },
-                default: res.send.bind(res)
-            });
-        });
-    
-    // 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 {
-                hr = putDocument(id,rev,'Response',doc);
-            }
-            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 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 getDocument(doc.surveyRunId,[],'SurveyRun')
-                        .handle({
-                            200: function(surveyRun) {
-                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
-                                    return deleteDocument(id,rev,doc);
-                                } else {
-                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
-                                }
-                            }
-                        });
-                    }
-                }
-            })
-            .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));
-        });
+    app.use('/api/open',
+            require('./api/open')(couch,schema).app);
 
     app.get('/api/mode',
-        ensureMIME(JSON_MIME),
+        apiUtil.ensureMIME(apiUtil.JSON_MIME),
         function(req,res){
             res.send({mode:env.mode});
@@ -961,41 +156,4 @@
 
     return app;
-}
-
-function writeObjectsToCSVStream(objects, stream, prelude) {
-    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);
-    if ( prelude ) {
-        _.forEach(prelude, function(val,key){
-            writer.writeRecord([key,val]);
-        });
-    }
-    writer.writeRecord(keys);
-    _.forEach(objects, function(obj){
-        var row = [];
-        _.forEach(obj, function(val,key){
-            row[idxs[key]] = val;
-        });
-        writer.writeRecord(row);
-    });
-}
-
-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/heroku.js
===================================================================
--- /Dev/trunk/src/server/bin/heroku.js	(revision 524)
+++ /Dev/trunk/src/server/bin/heroku.js	(revision 525)
@@ -6,5 +6,5 @@
 console.log("Using CouchDB on",env.couchServerURL+env.dbName);
 
-require('../app').App(env)
+require('../app').app(env)
 .then(function(app){
     app.listen(env.port, function() {
Index: /Dev/trunk/src/server/bin/upgrade-db-1-to-2.js
===================================================================
--- /Dev/trunk/src/server/bin/upgrade-db-1-to-2.js	(revision 524)
+++ /Dev/trunk/src/server/bin/upgrade-db-1-to-2.js	(revision 525)
@@ -1,15 +1,17 @@
 var env = require('../env')
+  , HTTPResult = require('../util/http-result')
   , 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){
+    if ( (doc.type === 'Response' || doc.type === 'SurveyRun') &&
+         !doc.secret ) {
+        return cryptoken()
+        .then(function(token){
             doc.secret = token;
             return doc;
         });
     } else {
-        return Q.reject();
+        return new HTTPResult(-1);
     }
 }).then(function(res){
Index: /Dev/trunk/src/server/bin/upgrade-db-2-to-3.js
===================================================================
--- /Dev/trunk/src/server/bin/upgrade-db-2-to-3.js	(revision 524)
+++ /Dev/trunk/src/server/bin/upgrade-db-2-to-3.js	(revision 525)
@@ -2,5 +2,5 @@
   , upgradeCouch = require('../config/upgrade-couchdb')
   , cryptoken = require('../util/crypto-token')
-  , Q = require('q')
+  , HTTPResult = require('../util/http-result')
   , _ = require('underscore')
   ;
@@ -51,5 +51,5 @@
         return doc;
     } else {
-        return Q.reject();
+        return new HTTPResult(-1);
     }
 }).then(function(res){
Index: /Dev/trunk/src/server/bin/upgrade-db-3-to-4.js
===================================================================
--- /Dev/trunk/src/server/bin/upgrade-db-3-to-4.js	(revision 524)
+++ /Dev/trunk/src/server/bin/upgrade-db-3-to-4.js	(revision 525)
@@ -2,5 +2,5 @@
   , upgradeCouch = require('../config/upgrade-couchdb')
   , cryptoken = require('../util/crypto-token')
-  , Q = require('q')
+  , HTTPResult = require('../util/http-result')
   , _ = require('underscore')
   ;
@@ -25,5 +25,5 @@
         return doc;
     } else {
-        return Q.reject();
+        return HTTPResult.fail();
     }
 }).then(function(res){
Index: /Dev/trunk/src/server/bin/upgrade-db-codes-2014-03-18.js
===================================================================
--- /Dev/trunk/src/server/bin/upgrade-db-codes-2014-03-18.js	(revision 525)
+++ /Dev/trunk/src/server/bin/upgrade-db-codes-2014-03-18.js	(revision 525)
@@ -0,0 +1,153 @@
+var env = require('../env')
+  , upgradeCouch = require('../config/upgrade-couchdb')
+  , cryptoken = require('../util/crypto-token')
+  , HTTPResult = require('../util/http-result')
+  , _ = require('underscore')
+  ;
+
+var qcodeMap = {
+    PT1GF: 'ST1GF'
+};
+
+var subcodeMap = {
+    SAPY: { // really called, team psychological safety
+        10: '1',
+        11: '2',
+        12: '3',
+        13: '4',
+        14: '5',
+        15: '6',
+        16: '7'
+    },
+    SAPI2: {
+        1: ''
+    },
+    PT1GF: {
+        10: '1',
+        11: '2',
+        12: '3',
+        13: '4',
+        14: '5',
+        15: '10',
+        16: '11'
+    },
+    SVAG12: {
+        0: ''
+    },
+    SVTCQ: {
+        10: '1',
+        11: '2',
+        12: '3',
+        13: '4',
+        14: '5',
+        15: '6',
+        16: '7',
+        17: '8',
+        18: '9',
+        19: '10',
+        110: '11',
+        111: '12',
+        112: '13',
+        113: '14',
+        114: '15',
+        115: '16'
+    },
+    SAGE: {
+        10: '1',
+        11: '2',
+        12: '3',
+        13: '4',
+        14: '5',
+        15: '6',
+        16: '7',
+        17: '8',
+        18: '9',
+        19: '10',
+        110: '11',
+        111: '12',
+        112: '13',
+        113: '14',
+        114: '15',
+        115: '16',
+        116: '17',
+        117: '18'
+    },
+    SAPI1: {
+        10: 'a',
+        11: 'b',
+        12: 'c',
+        13: 'd'
+    },
+    SVAG2: {
+        0: ''
+    },
+    GROUPID: {
+        0: ''
+    },
+    RESPONDENTID: {
+        0: ''
+    }
+};
+
+var fullcodeMap = {};
+_.each(subcodeMap, function(subcodes,oldCode){
+    var newCode = oldCode in qcodeMap ? qcodeMap[oldCode] : oldCode;
+    _.each(subcodes,function(newSubcode,oldSubcode){
+        fullcodeMap[oldCode+oldSubcode] = newCode+newSubcode;
+    });
+});
+
+function rewriteQuestion(question) {
+    if ( question.code in subcodeMap ) {
+        rewriteContent(question.content,subcodeMap[question.code]);
+    }
+    if ( question.code in qcodeMap ) {
+        question.code = qcodeMap[question.code];
+    }
+}
+
+function rewriteContent(obj,map) {
+    if ( _.isObject(obj) ) {
+        if ( 'subcode' in obj && obj.subcode in map ) {
+            obj.subcode = map[obj.subcode];
+        }
+        _.each(obj,function(item){
+            rewriteContent(item,map);
+        });
+    }
+}
+
+function rewriteAnswers(answers) {
+    _.each(answers,function(answer,code){
+        if ( code in fullcodeMap ) {
+            delete answers[code];
+            answers[fullcodeMap[code]] = answer;
+        }
+    });
+}
+
+function rewriteDoc(doc) {
+    switch(doc.type) {
+    case 'SurveyRun':
+        _.each(doc.survey.questions,rewriteQuestion);
+        return doc;
+    case 'Survey':
+        _.each(doc.questions,rewriteQuestion);
+        return doc;
+    case 'Question':
+        rewriteQuestion(doc);
+        return doc;
+    case 'Response':
+        rewriteAnswers(doc.answers);
+        return doc;
+    default:
+        return HTTPResult.fail();
+    }
+}
+
+upgradeCouch(env.couchServerURL,env.dbName,rewriteDoc)
+.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 524)
+++ /Dev/trunk/src/server/config/check-couchdb.js	(revision 525)
@@ -1,6 +1,7 @@
-var Q = require('q')
-  , _ = require('underscore')
+var _ = require('underscore')
+  , HTTPResult = require('../util/http-result')
   , validator = require('../util/validator')
-  , CouchDB = require('../util/couch').CouchDB
+  , CouchDB = require('../util/couch')
+  , objF = require('../util/object')
   ;
 
@@ -10,14 +11,4 @@
     var server = new CouchDB(couchServerURL,dbName);
     var designRe = /^_design\//;
-
-    var codes = {};
-    function codeUnique(code) {
-        if ( code in codes ) {
-            return false;
-        } else {
-            codes[code] = true;
-            return true;
-        }
-    }
 
     return server.get('/_all_docs')
@@ -32,14 +23,21 @@
                 .then(function(doc){
                     var valid = validator(doc,schema);
-                    if ( doc.type === "Question" && !codeUnique(doc.code) ) {
-                        valid.valid = false;
-                        valid.error = "Question code "+doc.code+" is not unique.";
-                    }
                     result[doc._id] = valid;
                     return result;
                 });
             });
-        }, Q.resolve({}))
+        }, new HTTPResult(200,{}))
         .value();
-    }).all();
+    }).then(function(result){
+        return server.get('_design/questions/_view/all_variables')
+        .then(function(variables){
+            var dups = objF.findDuplicates(
+                variables.rows,
+                function(row){ return row.key; });
+            result._duplicateVariables = _.groupBy(
+                dups,
+                function(dup){ return dup.key; });
+            return result;
+        });
+    });
 };
Index: /Dev/trunk/src/server/config/config-couchdb.js
===================================================================
--- /Dev/trunk/src/server/config/config-couchdb.js	(revision 524)
+++ /Dev/trunk/src/server/config/config-couchdb.js	(revision 525)
@@ -1,5 +1,5 @@
-var Q = require('q')
+var HTTPResult = require('../util/http-result')
   , _ = require('underscore')
-  , CouchDB = require('../util/couch').CouchDB
+  , CouchDB = require('../util/couch')
   , util = require('util')
   ;
@@ -62,5 +62,5 @@
                 }
             });
-        }, Q.resolve());
+        }, new HTTPResult(200));
     });
 };
Index: /Dev/trunk/src/server/config/couchdb-design-docs.js
===================================================================
--- /Dev/trunk/src/server/config/couchdb-design-docs.js	(revision 524)
+++ /Dev/trunk/src/server/config/couchdb-design-docs.js	(revision 525)
@@ -1,2 +1,28 @@
+var fs = require('fs')
+  , path = require('path')
+  ;
+
+function readModule(name) {
+    var filename;
+    if ( name.indexOf('.') === 0 ) {
+        filename = path.join(path.dirname(module.filename),name+'.js');
+    } else {
+        var dirname = path.join(path.dirname(module.filename),
+                                '../../node_modules',name);
+        filename = path.join(dirname,name+'.js');
+        var pkgfile = path.join(dirname,'package.json');
+        if ( fs.exists(pkgfile) ) {
+            var pkg = require(pkgfile);
+            if ( pkg.main ) {
+                filename = path.join(dirname,pkg.main);
+            }
+        }
+    }
+    var src = fs.readFileSync(filename, 'utf8');
+    src = src.replace(/require\s*\(\s*(['\"])([^'"]*)(['\"])\s*\)/g,
+                      'require($1views/lib/$2$3)');
+    return src;
+}
+
 module.exports = {
 
@@ -80,4 +106,32 @@
                 reduce: function(key, values, rereduce) { return sum(values); }
             },
+            all_variables: {
+                map: function(doc) {
+                    if ( doc.type !== 'Question' ) { return; }
+                    var _ = require('views/lib/underscore');
+                    var objF = require('views/lib/object');
+                    _.chain(objF.collectFields('subcode',doc.content))
+                    .map(function(subcode){
+                        return doc.code+subcode;
+                    })
+                    .each(function(variable){
+                        emit(variable,doc._id);
+                    });
+                }
+            },
+            published_variables: {
+                map: function(doc) {
+                    if ( doc.type !== 'Question' || !doc.publicationDate ) { return; }
+                    var _ = require('views/lib/underscore');
+                    var objF = require('views/lib/object');
+                    _.chain(objF.collectFields('subcode',doc.content))
+                    .map(function(subcode){
+                        return doc.code+subcode;
+                    })
+                    .each(function(variable){
+                        emit(variable,doc._id);
+                    });
+                }
+            },
             all_by_code: {
                 map: function(doc){
@@ -91,4 +145,8 @@
                     emit(doc.code,doc);
                 }
+            },
+            lib: {
+                underscore: readModule('underscore'),
+                object: readModule('../util/object')
             }
         }
Index: /Dev/trunk/src/server/config/couchdb-schema.json
===================================================================
--- /Dev/trunk/src/server/config/couchdb-schema.json	(revision 524)
+++ /Dev/trunk/src/server/config/couchdb-schema.json	(revision 525)
@@ -11,4 +11,5 @@
     "nonEmptyString": { "type": "string", "minLength": 1 },
     "codeString": { "type": "string", "pattern": "^[A-Za-z0-9]+$" },
+    "subcodeString": { "type": "string", "pattern": "^[A-Za-z0-9]*$" },
     "schemaInfo": {
       "type": "object",
@@ -148,5 +149,5 @@
         "properties": {
           "type": { "type": "string", "pattern": "^StringInput$" },
-          "subcode": { "$ref": "#/definitions/codeString" },
+          "subcode": { "$ref": "#/definitions/subcodeString" },
           "text": { "$ref": "#/definitions/nonEmptyString" }
         },
@@ -159,5 +160,5 @@
           "type": { "type": "string", "pattern": "^TextInput$" },
           "maxLength": { "type": "integer" },
-          "subcode": { "$ref": "#/definitions/codeString" },
+          "subcode": { "$ref": "#/definitions/subcodeString" },
           "text": { "$ref": "#/definitions/nonEmptyString" }
         },
@@ -172,5 +173,5 @@
           "max": { "type": "integer" },
           "places": { "type": "integer" },
-          "subcode": { "$ref": "#/definitions/codeString" },
+          "subcode": { "$ref": "#/definitions/subcodeString" },
           "text": { "$ref": "#/definitions/nonEmptyString" }
         },
@@ -192,5 +193,5 @@
               "minLabel": { "$ref": "#/definitions/nonEmptyString" },
               "maxLabel": { "$ref": "#/definitions/nonEmptyString" },
-              "subcode": { "$ref": "#/definitions/codeString" },
+              "subcode": { "$ref": "#/definitions/subcodeString" },
               "text": { "$ref": "#/definitions/nonEmptyString" }
             },
@@ -218,10 +219,10 @@
               "type": "object",
               "properties": {
-                  "subcode": { "$ref": "#/definitions/codeString" }
+                  "subcode": { "$ref": "#/definitions/subcodeString" }
               },
               "required": ["subcode"],
               "additionalProperties": false
           },
-          "subcode": { "$ref": "#/definitions/codeString" }
+          "subcode": { "$ref": "#/definitions/subcodeString" }
         },
         "required":["type","items","subcode"],
@@ -235,5 +236,5 @@
               "type": "object",
               "properties": {
-                "subcode": { "$ref": "#/definitions/codeString" },
+                "subcode": { "$ref": "#/definitions/subcodeString" },
                 "text": { "$ref": "#/definitions/nonEmptyString" }
               },
@@ -244,5 +245,5 @@
               "type": "object",
               "properties": {
-                "subcode": { "$ref": "#/definitions/codeString" }
+                "subcode": { "$ref": "#/definitions/subcodeString" }
               },
               "required": ["subcode"],
Index: /Dev/trunk/src/server/config/upgrade-couchdb.js
===================================================================
--- /Dev/trunk/src/server/config/upgrade-couchdb.js	(revision 524)
+++ /Dev/trunk/src/server/config/upgrade-couchdb.js	(revision 525)
@@ -1,5 +1,5 @@
-var Q = require('q')
+var HTTPResult = require('../util/http-result')
   , _ = require('underscore')
-  , CouchDB = require('../util/couch').CouchDB
+  , CouchDB = require('../util/couch')
   ;
 
@@ -19,5 +19,5 @@
                 return server.get(doc.id)
                 .then(function(doc){
-                    Q.when(upgrade(doc))
+                    HTTPResult.when(200,upgrade(doc))
                     .then(function(newDoc){
                         return server.put(doc._id,newDoc)
@@ -32,5 +32,5 @@
                 });
             });
-        }, Q.resolve({}))
+        }, new HTTPResult(200,{}))
         .value();
     }).all();
Index: /Dev/trunk/src/server/util/couch.js
===================================================================
--- /Dev/trunk/src/server/util/couch.js	(revision 524)
+++ /Dev/trunk/src/server/util/couch.js	(revision 525)
@@ -1,8 +1,5 @@
-var CouchDB, _ref,
-  __hasProp = {}.hasOwnProperty;
-
-var request = require('./q-request')
+var request = require('./request')
   , _ = require('underscore')
-  , Q = require('q');
+  ;
 
 function CouchDB(url, db) {
@@ -58,26 +55,9 @@
         if (opts.headers) { _.extend(options.headers, opts.headers); }
     }
-    var req = request(url, options);
-    var res = req.response
-    .then(function(res) {
-        return req.then(function(res) {
-            return JSON.parse(res);
-        }, function(err) {
-            return Q.reject(JSON.parse(err));
+    return request(url, options)
+        .handle({
+            '-1': _.identity,
+            default: function(status,result) { return JSON.parse(result); }
         });
-    }, function(err) {
-        return Q.reject(err);
-    });
-    res.response = req.response
-    .then(function(res) {
-        return {
-            statusCode: res.statusCode,
-            headers: res.headers,
-            body: JSON.parse(res.body)
-        };
-    }, function(err) {
-        return Q.reject({error:err.message});
-    });
-    return res;
 }
 
@@ -108,3 +88,3 @@
 }
 
-exports.CouchDB = CouchDB;
+module.exports = CouchDB;
Index: /Dev/trunk/src/server/util/crypto-token.js
===================================================================
--- /Dev/trunk/src/server/util/crypto-token.js	(revision 524)
+++ /Dev/trunk/src/server/util/crypto-token.js	(revision 525)
@@ -1,14 +1,12 @@
-var Q = require('q')
+var HTTPResult = require('./http-result')
   , 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;
+    bytes  =bytes || 8;
+    var result = new HTTPResult();
+    crypto.randomBytes(bytes, result.asCallback(201));
+    return result
+        .then(function(buf){
+            return buf.toString('hex');
+        });
 };
Index: /Dev/trunk/src/server/util/http-result.js
===================================================================
--- /Dev/trunk/src/server/util/http-result.js	(revision 524)
+++ /Dev/trunk/src/server/util/http-result.js	(revision 525)
@@ -1,4 +1,14 @@
-// <init> :: status? -> result? -> HTTPResult
+var _ = require('underscore')
+  ;
+
+var when;
+
+// type Callback = status:Number -> result:Any -> Either HTTPResult Any
+// type Handler = { f:Callback, next:HTTPResult }
+
+// <init> :: status:Number? -> result:Any? -> HTTPResult
+var counter = 0;
 function HTTPResult(status,result) {
+    this.id = counter++;
     this.status = 0;
     this.result = null;
@@ -9,19 +19,4 @@
 }
 
-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() {
@@ -29,32 +24,34 @@
 };
 
-// set :: status -> result? -> ()
+// set :: status:Number -> result:Any? -> ()
 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."); }
+    if ( this.isSet() ) {
+        throw new Error("Result was already set.");
+    }
+    if ( !_.isNumber(status) ) {
+        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]);
+    _.each(this._handlers,this._fire.bind(this));
+};
+
+// _fire :: Handler -> ()
+HTTPResult.prototype._fire = function(handler) {
+    var ret = handler.f(this.status,this.result);
+    if ( ret instanceof HTTPResult ) {
+        ret.handle(handler.next.set.bind(handler.next));
+    } else {
+        handler.next.set(this.status,ret);
     }
 };
 
-// _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)}
+/* handle :: Either Callback
+ *                  {status:Number*: (result -> Either HTTPResult Any),
+ *                   'default'?: Callback}
  *           -> HTTPResult
  */
 HTTPResult.prototype.handle = function(fOrObj) {
-    var f = typeof fOrObj === 'function' ?
+    var f = _.isFunction(fOrObj) ?
             fOrObj :
             function(status,result) {
@@ -67,11 +64,64 @@
                 }
             };
-    f._next = new HTTPResult();
+    var next = new HTTPResult();
+    var handler = {
+        f: f,
+        next: next
+    };
     if ( this.isSet() ) {
-        this._fire(f);
+        this._fire(handler);
     } else {
-        this._handlers.push(f);
+        this._handlers.push(handler);
     }
-    return f._next;
+    return next;
+};
+
+// then :: onSuccess:(result:Any -> status:Number)? ->
+//         onError:(result:Any -> status:Number)? -> HTTPResult
+HTTPResult.prototype.then = function(onSuccess,onError) {
+    var f = function(status,result) {
+        if ( status >= 200 && status < 300 && onSuccess ) {
+            if ( onSuccess ) {
+                result = onSuccess(result,status);
+            }
+        } else {
+            if ( onError ) {
+                result = onError(result,status);
+            }
+        }
+        return result;
+    };
+    return this.handle(f);
+};
+
+HTTPResult.prototype.asCallback = function(status) {
+    status = status || 200;
+    return function(ex,result) {
+        if ( ex ) {
+            this.set(-1,ex);
+        } else {
+            this.set(status,result);
+        }
+    }.bind(this);
+};
+
+// when :: Either HTTPResult Any -> HTTPResult
+when = HTTPResult.when = function(status,valueOrResult) {
+    status = status || 200;
+    if ( valueOrResult instanceof HTTPResult ) {
+        return valueOrResult;
+    } else {
+        return new HTTPResult(status,valueOrResult);
+    }
+};
+
+// ok :: result:Any? -> HTTPResult
+HTTPResult.ok = function(result) {
+    return new HTTPResult(200,result);
+};
+
+// fail :: error:Any? -> HTTPResult
+HTTPResult.fail = function(error) {
+    return new HTTPResult(-1,error);
 };
 
Index: /Dev/trunk/src/server/util/object.js
===================================================================
--- /Dev/trunk/src/server/util/object.js	(revision 525)
+++ /Dev/trunk/src/server/util/object.js	(revision 525)
@@ -0,0 +1,30 @@
+var _ = require('underscore')
+  ;
+
+var exportFields = exports.collectFields = function collectFields(field,obj) {
+    if ( _.isObject(obj) ) {
+        return _.reduce(obj,function(vals,val,key){
+            if ( key === field ) {
+                vals.push(val);
+            }
+            return vals.concat(collectFields(field,val));
+        },[]);
+    } else {
+        return [];
+    }
+};
+
+var findDuplicates = exports.findDuplicates = function(array,iterator) {
+    iterator = iterator || _.identity;
+    array = _.sortBy(array,iterator);
+    return _.chain(_.initial(array))
+            .zip(_.rest(array))
+            .reduce(function(dups,nAndSn){
+                if ( iterator(nAndSn[0]) === iterator(nAndSn[1]) &&
+                     nAndSn[0] !== _.last(dups) ) {
+                    return dups.concat([nAndSn[0]]);
+                }
+                return dups;
+            },[])
+            .value();
+};
Index: v/trunk/src/server/util/q-request.js
===================================================================
--- /Dev/trunk/src/server/util/q-request.js	(revision 524)
+++ 	(revision )
@@ -1,30 +1,0 @@
-var q = require('q'),
-    request = require('request'),
-    url = require('url'),
-    _ = require('underscore');
-
-module.exports = function(url, options) {
-
-    var dfd_body = q.defer();
-    var dfd_response = q.defer();
-    dfd_body.promise.response = dfd_response.promise;
-    
-    options = options ? _.clone(options) : {};
-    options.uri = url || options.uri;
-    
-    request(options,function(err,res,body){
-        if ( err ) {
-            dfd_response.reject(err);
-        } else {
-            dfd_response.resolve(res);
-            if ( res.statusCode >= 200 && res.statusCode < 300 ) {
-                dfd_body.resolve(body);
-            } else {
-                dfd_body.reject(body);
-            }
-        }
-    });
-
-    return dfd_body.promise;
-    
-};
Index: /Dev/trunk/src/server/util/request.js
===================================================================
--- /Dev/trunk/src/server/util/request.js	(revision 525)
+++ /Dev/trunk/src/server/util/request.js	(revision 525)
@@ -0,0 +1,24 @@
+var HTTPResult = require('./http-result')
+  , request = require('request')
+  , url = require('url')
+  , _ = require('underscore')
+  ;
+
+module.exports = function(url, options) {
+
+    var result = new HTTPResult();
+    
+    options = options ? _.clone(options) : {};
+    options.uri = url || options.uri;
+    
+    request(options,function(err,res,body){
+        if ( err ) {
+            result.set(-1,err);
+        } else {
+            result.set(res.statusCode,body);
+        }
+    });
+
+    return result;
+    
+};
