Changeset 525


Ignore:
Timestamp:
03/19/14 21:33:13 (11 years ago)
Author:
hendrikvanantwerpen
Message:
  • Allow empty subcodes.
  • Use HTTPResult exclusively on server (no more q).
  • Set readonly & disabled on ourselves as well in _ComplexValueMixin
  • Split server into several modules.
  • Check codes on the variable level, not question level.
  • We can add modules in design documents now.
Location:
Dev/trunk/src
Files:
12 added
2 deleted
24 edited

Legend:

Unmodified
Added
Removed
  • Dev/trunk/src/client/qed-client/model/classes/_Class.js

    r510 r525  
    7171            } else if ( lang.isObject(obj) ) {
    7272                objectFuns.forEach(obj,function(v,prop){
    73                     if ( v === null ||
    74                          v === "" ||
    75                          (typeof v === "number" && isNaN(v)) ) {
     73                    if ( ( v === null ||
     74                           v === "" ||
     75                           (typeof v === "number" && isNaN(v)) ) &&
     76                         prop !== 'subcode' ) // HACK : this hardcoded exclusion for subcode is quite nasty
     77                    {
    7678                        delete obj[prop];
    7779                    } else {
  • Dev/trunk/src/client/qed-client/model/widgets/QuestionEditorPreviewItem.js

    r513 r525  
    149149            if ( this.innerWidget !== null ) {
    150150                if ( this._editing === true ) {
    151                     this._onChange();
    152                     // how to force event on widget here?
     151                    // because the widget doesn't generate an event
     152                    // unless it loses focus, we set the value here
     153                    // just to be sure not to lose any user input
     154                    this._setValueInternal(this.innerWidget.get('value'));
    153155                }
    154156                this.removeChild(this.innerWidget);
  • Dev/trunk/src/client/qed-client/model/widgets/questions/ScaleInputWidget.js

    r511 r525  
    2222            this.minNode.innerHTML = this.minLabel || "";
    2323            this.maxNode.innerHTML = this.maxLabel || "";
    24             if ( this.naLabel !== null ) {
     24            if ( this.naLabel ) {
    2525                this.naNode.innerHTML = this.naLabel;
    2626            }
     
    5656                    className: 'max'
    5757                }, tr);
    58                 if ( this.naLabel !== null && this.naLabel !== "" ) {
     58                if ( this.naLabel ) {
    5959                    td = domConstruct.create("td", {}, tr);
    6060                    radio = new RadioButton({
  • Dev/trunk/src/client/qed-client/model/widgets/questions/templates/MultipleChoiceInputConfigWidget.html

    r511 r525  
    33    <label class="qedLabel" for="subcode">Subcode</label>
    44    <input data-dojo-type="dijit/form/ValidationTextBox"
    5            data-dojo-props="required:true,placeHolder:'Subcode'"
     5           data-dojo-props="placeHolder:'Subcode'"
    66           class="subcode"
    77           name="subcode"/>
  • Dev/trunk/src/client/qed-client/model/widgets/questions/templates/NumberInputConfigWidget.html

    r510 r525  
    55         data-dojo-attach-point="subcodeBox"
    66         data-dojo-type="dijit/form/ValidationTextBox"
    7          data-dojo-props="required: true, placeholder: 'Subcode'"
     7         data-dojo-props="placeholder: 'Subcode'"
    88         name="subcode"></div>
    99  </div>
  • Dev/trunk/src/client/qed-client/model/widgets/questions/templates/ScaleInputConfigRowWidget.html

    r506 r525  
    22  <th class="subcode">
    33    <div data-dojo-type="dijit/form/ValidationTextBox"
    4          data-dojo-props="required: true, placeholder: 'Subcode'"
     4         data-dojo-props="placeholder: 'Subcode'"
    55         name="subcode"></div>
    66  </th>
  • Dev/trunk/src/client/qed-client/model/widgets/questions/templates/StringInputConfigWidget.html

    r510 r525  
    55         data-dojo-attach-point="subcodeBox"
    66         data-dojo-type="dijit/form/ValidationTextBox"
    7          data-dojo-props="required: true, placeholder: 'Subcode'"
     7         data-dojo-props="placeholder: 'Subcode'"
    88         name="subcode"></div>
    99  </div>
  • Dev/trunk/src/client/qed-client/model/widgets/questions/templates/TextInputConfigWidget.html

    r510 r525  
    55         data-dojo-attach-point="subcodeBox"
    66         data-dojo-type="dijit/form/ValidationTextBox"
    7          data-dojo-props="required: true, placeholder: 'Subcode'"
     7         data-dojo-props="placeholder: 'Subcode'"
    88         name="subcode"></div>
    99  </div>
  • Dev/trunk/src/client/qed-client/widgets/ListWidget.js

    r513 r525  
    9898        _getValueAttr: function() {
    9999            return array.map(
    100                 this._getDescendantFormWidgets(),
    101                 function(child) { return child.get('value'); }
    102                 ,this);
     100                this.source.getAllNodes(),
     101                lang.hitch(this,function(node){
     102                    var widget = registry.byNode(node);
     103                    if ( widget && 'value' in widget ) {
     104                        return widget.get('value');
     105                    } else {
     106                        return this.source.getItem(node.id).data;
     107                    }
     108                }));
    103109        },
    104110        _setValueAttr: function(value,priorityChange) {
  • Dev/trunk/src/client/qed-client/widgets/_ComplexValueMixin.js

    r513 r525  
    6464                    if (typeof widgets[0].checked === 'boolean') {
    6565                        array.forEach(widgets, function(w){
    66                             w.set('value', array.indexOf(values, w._get('value')) !== -1, priorityChange);
     66                            w.set('value',
     67                                  array.indexOf(values,w._get('value'))!==-1,
     68                                  priorityChange);
    6769                        });
    6870                    } else if (widgets[0].multiple) {
     
    9092        _setDisabledAttr: function(disabled) {
    9193            this.inherited(arguments);
     94            this._set('disabled',disabled);
    9295            array.forEach(
    9396                this._getDescendantFormWidgets(),
     
    98101        _setReadOnlyAttr: function(readOnly) {
    99102            this.inherited(arguments);
     103            this._set('readOnly',readOnly);
    100104            array.forEach(
    101105                this._getDescendantFormWidgets(),
  • Dev/trunk/src/package.json

    r489 r525  
    88    "passport": "~0.1.18",
    99    "passport-local": "~0.1.6",
    10     "q": "~0.9.7",
    1110    "request": "~2.21.0",
    1211    "ya-csv": "~0.9.3",
  • Dev/trunk/src/server/app.js

    r523 r525  
    55  , path = require("path")
    66  , proxy = require("./util/simple-http-proxy")
    7   , CSV = require("ya-csv")
    8   , CouchDB = require('./util/couch').CouchDB
     7  , CouchDB = require('./util/couch')
     8  , HTTPResult = require('./util/http-result')
    99  , _ = require("underscore")
    10   , validator = require("./util/validator")
    11   , HTTPResult = require("./util/http-result")
    12   , etags = require("./util/etags")
    13   , cryptoken = require('./util/crypto-token')
    14   , Q = require('q')
    1510  ;
    1611
    17 exports.App = function(env) {
     12exports.app = function(env) {
    1813
    1914    assertSetting("couchServerURL", env, _.isString);
     
    2722    .then(function(schemaInfo){
    2823        if ( schemaInfo.version !== schema.version ) {
    29             return Q.reject(new Error("Found schema version "+schemaInfo.version+
    30                                       ", expected "+schema.version));
     24            return HTTPResult.fail(
     25                new Error("Found schema version "+schemaInfo.version+
     26                          ", expected "+schema.version));
    3127        } else if ( schemaInfo.viewsVersion !== views.schemaInfo.viewsVersion ) {
    32             return Q.reject(new Error("Found views version "+schemaInfo.viewsVersion+
    33                                       ", expected "+views.schemaInfo.viewsVersion));
     28            return HTTPResult.fail(
     29                new Error("Found views version "+schemaInfo.viewsVersion+
     30                          ", expected "+views.schemaInfo.viewsVersion));
    3431        } else {
    3532            return configureApp(env,couch,schema);
     
    4037
    4138function configureApp(env,couch,schema) {
     39
     40    var apiUtil = require('./api/util')(couch,schema);
    4241
    4342    function clientPath(relativePath) {
     
    9190
    9291    // static resources
    93     app.get('/', function(request, response){
    94         response.sendfile(clientPath('index.html'));
     92    app.get('/', function(req, res){
     93        res.sendfile(clientPath('index.html'));
    9594    });
    96     app.get('/*.html', function(request, response) {
    97         response.sendfile(clientPath(request.path));
     95    app.get('/*.html', function(req, res) {
     96        res.sendfile(clientPath(req.path));
    9897    });
    9998    _.each(['/dojo', '/dijit', '/dojox', '/qed', '/qed-client'], function(dir){
     
    122121        });
    123122
    124     var JSON_MIME = 'application/json';
    125     var CSV_MIME = 'application/json';
    126     function ensureMIME(mimeType) {
    127         return function(req,res,next) {
    128             if (!req.accepts(mimeType)) {
    129                 res.send(406);
    130             } else {
    131                 res.set({
    132                     'Content-Type': mimeType
    133                 });
    134                 next();
    135             }
    136         };
    137     }
    138    
    139     function identity(obj) { return obj; }
    140     function stripAndReturnPrivates(obj) {
    141         var priv = {};
    142         _.each(obj||{},function(val,key){
    143             if (key.substring(0,1) === '_') {
    144                 priv[key] = val;
    145                 delete obj[key];
    146             }
    147         });
    148         return priv;
    149     }
    150     function areDocsUnique(docs) {
    151         return _.chain(docs)
    152                 .map(function(doc){ return doc._id; })
    153                 .uniq()
    154                 .value().length === docs.length;
    155     }
    156     function areDocsPublished(docs) {
    157         return _.every(docs, isDocPublished);
    158     }
    159     function isDocPublished(doc) {
    160         return doc.publicationDate;
    161     }
     123    app.use('/api/questions', ensureAuthenticated);
     124    app.use('/api/questions',
     125            require('./api/questions')(couch,schema).app);
    162126
    163     function handleUnknownResponse(status,error) {
    164         return new HTTPResult(500,{error: error.reason});
    165     }
    166     function handleUnknownError(error) {
    167         return new HTTPResult(500, {error: "Unknown error", innerError: error});
    168     }
    169     function handleRowValues(result) {
    170         return _.map(result.rows, function(item) { return item.value; });
    171     }
    172     function handleRowDocs(result) {
    173         return _.map(result.rows, function(item) { return item.doc; });
    174     }
    175     function handleRowFirstDoc(result) {
    176         if ( result.rows.length > 0 ) {
    177             return result.rows[0].doc;
    178         } else {
    179             return new HTTPResult(404,{error:"No document found."});
    180         }
    181     }
    182     function handleRowFirstValue(result) {
    183         if ( result.rows.length > 0 ) {
    184             return result.rows[0].value;
    185         } else {
    186             return new HTTPResult(404,{error:"No document found."});
    187         }
    188     }
    189     function handleRowDictOfDocs(result) {
    190         return _.reduce(result.rows, function(dict,item) {
    191             dict[item.key] = item.doc;
    192             return dict;
    193         }, {});
    194     }
    195     function handleRowDictOfValues(result) {
    196         return _.reduce(result.rows, function(dict,item) {
    197             dict[item.key] = item.value;
    198             return dict;
    199         }, {});
    200     }
     127    app.use('/api/categories', ensureAuthenticated);
     128    app.use('/api/categories',
     129            require('./api/categories')(couch,schema).app);
    201130
    202     function getDocumentsOfType (type) {
    203         var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
    204         return HTTPResult.fromResponsePromise(couch.get(url).response,
    205                                               handleUnknownError)
    206         .handle({
    207             200: handleRowValues,
    208             404: function() { return {error: "Cannot find collection of type "+type}; },
    209             default: handleUnknownResponse
    210         });
    211     }
    212     function getDocument(id,rev,type) {
    213         var opts = {headers:{}};
    214         if (rev) {
    215             opts.headers['If-Non-Match'] = '"'+rev+'"';
    216         }
    217         return HTTPResult.fromResponsePromise(couch.get(id,opts).response,
    218                                               handleUnknownError)
    219         .handle({
    220             200: function(doc){
    221                 if ( doc.type !== type ) {
    222                     return new HTTPResult(404,{error:"Document not found."});
    223                 } else {
    224                     var priv = stripAndReturnPrivates(doc);
    225                     if ( priv._rev !== rev ) {
    226                         doc._id = priv._id;
    227                         doc._rev = priv._rev;
    228                         return doc;
    229                     } else {
    230                         return new HTTPResult(304);
    231                     }
    232                 }
    233             },
    234             304: identity,
    235             default: handleUnknownResponse
    236         });
    237     }
    238     function putDocument(id,rev,type,doc) {
    239         var priv = stripAndReturnPrivates(doc);
    240         var valid;
    241         if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
    242             var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
    243             return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response,
    244                                                   handleUnknownError)
    245             .handle({
    246                 201: function(res){
    247                     doc._id = res.id;
    248                     doc._rev = res.rev;
    249                     return doc;
    250                 },
    251                 409: function(error) {
    252                     return {error: error.reason};
    253                 },
    254                 default: handleUnknownResponse
    255             });
    256         } else {
    257             return new HTTPResult(400,{error: "Document failed schema verification.", valid: valid});
    258         }
    259     }
    260     function deleteDocument(id,rev) {
    261         if ( rev ) {
    262             var opts = {headers:{'If-Match':'"'+rev+'"'}};
    263             return HTTPResult.fromResponsePromise(couch.delete(id,opts).response,
    264                                                   handleUnknownError)
    265             .handle({
    266                 200: identity,
    267                 409: function(error) {
    268                     return {error: error.reason};
    269                 },
    270                 default: handleUnknownResponse
    271             });
    272         } else {
    273             return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
    274         }
    275     }
    276     function postDocument(type,doc) {
    277         var priv = stripAndReturnPrivates(doc);
    278         var valid;
    279         if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
    280             return HTTPResult.fromResponsePromise(couch.post(doc).response,
    281                                                   handleUnknownError)
    282             .handle({
    283                 201: function(response) {
    284                     doc._id = response.id;
    285                     doc._rev = response.rev;
    286                     return doc;
    287                 },
    288                 default: handleUnknownResponse
    289             });
    290         } else {
    291             return new HTTPResult(400,{error: "Document failed schema verification.", valid: valid});
    292         }
    293     }
     131    app.use('/api/topics', ensureAuthenticated);
     132    app.use('/api/topics',
     133            require('./api/topics')(couch,schema).app);
    294134
    295     function makeDocsGet(type) {
    296         return function(req,res) {
    297             getDocumentsOfType(type)
    298             .handle(res.send.bind(res));
    299         };
    300     }
    301     function makeDocGet_id(type) {
    302         return function(req,res) {
    303             var id = req.params.id;
    304             var rev = etags.parse(req.header('If-Non-Match'))[0];
    305             getDocument(id,rev,type)
    306             .handle({
    307                 200: function(doc){
    308                     res.set({
    309                         'ETag': etags.format([doc._rev])
    310                     }).send(200, doc);
    311                 },
    312                 default: res.send.bind(res)
    313             });
    314         };
    315     }
    316     function makeDocPut_id(type) {
    317         return function(req,res) {
    318             var id = req.params.id;
    319             var doc = req.body;
    320             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    321             putDocument(id,rev,type,doc)
    322             .handle({
    323                 201: function(doc) {
    324                     res.set({
    325                         'ETag': etags.format([doc._rev])
    326                     }).send(201, doc);
    327                 },
    328                 default: res.send.bind(res)
    329             });
    330         };
    331     }
    332     function makeDocDel_id(type) {
    333         return function(req,res) {
    334             var id = req.params.id;
    335             var doc = req.body;
    336             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    337             deleteDocument(id,rev)
    338             .handle(res.send.bind(res));
    339         };
    340     }
    341     function makeDocPost(type) {
    342         return function(req,res) {
    343             var doc = req.body;
    344             postDocument(type,doc)
    345             .handle({
    346                 201: function(doc) {
    347                     res.set({
    348                         'ETag': etags.format([doc._rev])
    349                     }).send(201, doc);
    350                 },
    351                 default: res.send.bind(res)
    352             });
    353         };
    354     }
     135    app.use('/api/surveys', ensureAuthenticated);
     136    app.use('/api/surveys',
     137            require('./api/surveys')(couch,schema).app);
    355138
    356     // Questions
    357     function getQuestionsWithCode(code,sub) {
    358         var url = '_design/questions/_view/'+(sub||'all')+'_by_code';
    359         var query = {include_docs:true,key:code};
    360         return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
    361                                               handleUnknownError)
    362         .handle({
    363             200: handleRowDocs,
    364             default: handleUnknownResponse
    365         });
    366     }
    367     function getQuestions(sub) {
    368         var url = '_design/questions/_view/'+(sub||'all');
    369         var query = {include_docs:true};
    370         return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
    371                                               handleUnknownError)
    372         .handle({
    373             200: handleRowDocs,
    374             default: handleUnknownResponse
    375         });
    376     }
    377     function getQuestionsAndCodes() {
    378         var url = '_design/questions/_view/by_code';
    379         var query = {include_docs:true};
    380         return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
    381                                               handleUnknownError)
    382         .handle({
    383             200: handleRowDictOfDocs,
    384             default: handleUnknownResponse
    385         });
    386     }
    387     function getQuestionsWithTopic(topic,sub) {
    388         var url = '_design/questions/_view/'+(sub||'all')+'_topics';
    389         var query = {reduce:false,include_docs:true,key:topic};
    390         return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
    391                                               handleUnknownError)
    392         .handle({
    393             200: handleRowDocs,
    394             default: handleUnknownResponse
    395         });
    396     }
    397     function getQuestionsWithCategoryAndTopic(category,topic,sub) {
    398         var hasTopic = typeof topic !== 'undefined';
    399         var url = '_design/questions/_view/'+(sub||'all');
    400         var query = {reduce:false,include_docs:true,
    401                      startkey:hasTopic?[category,topic]:[category],
    402                      endkey:hasTopic?[category,topic]:[category,{}]};
    403         return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
    404                                               handleUnknownError)
    405         .handle({
    406             200: handleRowDocs,
    407             default: handleUnknownResponse
    408         });
    409     }
    410     function collectFields(field,obj) {
    411         if ( _.isArray(obj) ) {
    412             return _.reduce(obj,function(subcodes,val,idx){
    413                 return subcodes.concat(collectFields(field,val));
    414             },[]);
    415         } else if ( _.isObject(obj) ) {
    416             return _.reduce(obj,function(subcodes,val,key){
    417                 if ( key === field ) {
    418                     subcodes.push(val);
    419                 }
    420                 return subcodes.concat(collectFields(field,val));
    421             },[]);
    422         } else {
    423             return [];
    424         }
    425     }
    426     function areSubcodesUnique(content) {
    427         var subcodes = collectFields('subcode',content);
    428         return _.uniq(subcodes).length === subcodes.length;
    429     }
    430     app.get('/api/questions',
    431         ensureAuthenticated,
    432         ensureMIME(JSON_MIME),
    433         function(req,res) {
    434             var sub = 'published' in req.query && 'published';
    435             var hr;
    436             if ( 'category' in req.query ) {
    437                 hr = getQuestionsWithCategoryAndTopic(req.query.category,
    438                                                       req.query.topic,sub);
    439             } else if ( 'topic' in req.query ) {
    440                 hr = getQuestionsWithTopic(req.query.topic,sub);
    441             } else if ( 'code' in req.query ) {
    442                 hr = getQuestionsWithCode(req.query.code,sub);
    443             } else {
    444                 hr = getQuestions(sub);
    445             }
    446             hr.handle(res.send.bind(res));
    447         });
    448     app.post('/api/questions',
    449         ensureAuthenticated,
    450         ensureMIME(JSON_MIME),
    451         function (req,res) {
    452             var doc = req.body;
    453             getQuestionsWithCode(doc.code)
    454             .handle({
    455                 200: function(others) {
    456                     if ( others.length > 0 ) {
    457                         return new HTTPResult(403,{error:"Other question with this code already exists."});
    458                     } else if ( !areSubcodesUnique(doc.content) ) {
    459                         return new HTTPResult(400,{error:"Subcodes are not unqiue."});
    460                     } else {
    461                         return postDocument('Question',doc);
    462                     }
    463                 }
    464             })
    465             .handle(res.send.bind(res));
    466         });
    467     app.get('/api/questions/:id',
    468         ensureAuthenticated,
    469         ensureMIME(JSON_MIME),
    470         makeDocGet_id('Question'));
    471     app.put('/api/questions/:id',
    472         ensureAuthenticated,
    473         ensureMIME(JSON_MIME),
    474         function (req,res) {
    475             var id = req.params.id;
    476             var doc = req.body;
    477             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    478             getQuestionsWithCode(doc.code)
    479             .handle({
    480                 200: function(others) {
    481                     if ( others.length > 0 && _.some(others,function(other){ return other._id !== id; })  ) {
    482                         return new HTTPResult(403,{error:"Other question with this code already exists."});
    483                     } else if ( !areSubcodesUnique(doc.content) ) {
    484                         return new HTTPResult(400,{error:"Subcodes are not unqiue."});
    485                     } else {
    486                         return putDocument(id,rev,'Question',doc);
    487                     }
    488                 }
    489             })
    490             .handle(res.send.bind(res));
    491         });
    492     app.delete('/api/questions/:id',
    493         ensureAuthenticated,
    494         ensureMIME(JSON_MIME),
    495         makeDocDel_id('Question'));
     139    app.use('/api/surveyRuns', ensureAuthenticated);
     140    app.use('/api/surveyRuns',
     141            require('./api/surveyRuns')(couch,schema).app);
    496142
    497     // Categories and topics
    498     function getTopics(sub) {
    499         var url = '_design/questions/_view/'+(sub||'all')+'_topics';
    500         return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
    501                                             handleUnknownError)
    502         .handle({
    503             200: function(result) {
    504                 return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
    505             },
    506             default: handleUnknownResponse
    507         });
    508     }
    509     function getCategories(sub) {
    510         var url = '_design/questions/_view/'+(sub||'all');
    511         return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
    512                                        handleUnknownError)
    513         .handle({
    514             200: function(result) {
    515                 return _.map(result.rows, function(item) {
    516                     return { name:item.key[0], count:item.value };
    517                 });
    518             },
    519             default: handleUnknownResponse
    520         });
    521     }
    522     function getTopicsWithCategory(category,sub) {
    523         var url = '_design/questions/_view/'+(sub||'all');
    524         return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
    525                                        handleUnknownError)
    526         .handle({
    527             200: function(result) {
    528                 return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
    529             },
    530             default: handleUnknownResponse
    531         });
    532     }
    533     app.get('/api/categories',
    534         ensureAuthenticated,
    535         ensureMIME(JSON_MIME),
    536         function(req,res) {
    537             var sub = 'published' in req.query && 'published';
    538             getCategories(sub)
    539             .handle(res.send.bind(res));
    540         });
    541     app.get('/api/categories/:category/topics',
    542         ensureAuthenticated,
    543         ensureMIME(JSON_MIME),
    544         function(req,res) {
    545             var category = req.params.category;
    546             var sub = 'published' in req.query && 'published';
    547             getTopicsWithCategory(category,sub)
    548             .handle(res.send.bind(res));
    549         });
    550     app.get('/api/topics',
    551         ensureAuthenticated,
    552         ensureMIME(JSON_MIME),
    553         function(req,res) {
    554             var sub = 'published' in req.query && 'published';
    555             var hr;
    556             if ( 'category' in req.query ) {
    557                 hr = getTopicsWithCategory(req.query.category,sub);
    558             } else {
    559                 hr = getTopics(sub);
    560             }
    561             hr.handle(res.send.bind(res));
    562         });
     143    app.use('/api/responses', ensureAuthenticated);
     144    app.use('/api/responses',
     145            require('./api/responses')(couch,schema).app);
    563146
    564     // Surveys
    565     app.get('/api/surveys',
    566         ensureAuthenticated,
    567         ensureMIME(JSON_MIME),
    568         function(req,res) {
    569             var url;
    570             if ( 'drafts' in req.query ) {
    571                 url = '_design/surveys/_view/drafts';
    572             } else if ( 'published' in req.query ) {
    573                 url = '_design/surveys/_view/published';
    574             } else {
    575                 url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
    576             }
    577             HTTPResult.fromResponsePromise(couch.get(url).response,
    578                                            handleUnknownError)
    579             .handle({
    580                 200: function(result) {
    581                     return _.map(result.rows, function(item) { return item.value; });
    582                 },
    583                 default: handleUnknownResponse
    584             })
    585             .handle(res.send.bind(res));
    586         });
    587     app.post('/api/surveys',
    588         ensureAuthenticated,
    589         ensureMIME(JSON_MIME),
    590         function(req,res) {
    591             var doc = req.body;
    592             var hr;
    593             if ( !areDocsUnique(doc.questions) ) {
    594                 hr = new HTTPResult(400,{error:"Survey contains duplicate questions."});
    595             } else if ( isDocPublished(doc) && !areDocsPublished(doc.questions) ) {
    596                 hr = new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."});
    597             } else {
    598                 hr = postDocument('Survey',doc);
    599             }
    600             hr.handle(res.send.bind(res));
    601         });
    602     app.get('/api/surveys/:id',
    603         ensureAuthenticated,
    604         ensureMIME(JSON_MIME),
    605         makeDocGet_id('Survey'));
    606     app.put('/api/surveys/:id',
    607         ensureAuthenticated,
    608         ensureMIME(JSON_MIME),
    609         function(req,res) {
    610             var id = req.params.id;
    611             var doc = req.body;
    612             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    613             if ( !areDocsUnique(doc.questions) ) {
    614                 new HTTPResult(400,{error:"Survey contains duplicate questions."})
    615                 .handle(res.send.bind(res));
    616             } else if ( isDocPublished(doc) && !areDocsPublished(doc.questions) ) {
    617                 new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."})
    618                 .handle(res.send.bind(res));
    619             } else {
    620                 putDocument(id,rev,'Survey',doc)
    621                 .handle({
    622                     201: function(doc) {
    623                         res.set({
    624                             'ETag': etags.format([doc._rev])
    625                         }).send(201, doc);
    626                     },
    627                     default: res.send.bind(res)
    628                 });
    629             }
    630         });
    631     app.delete('/api/surveys/:id',
    632         ensureAuthenticated,
    633         ensureMIME(JSON_MIME),
    634         makeDocDel_id('Survey'));
    635 
    636     // SurveyRuns
    637     app.get('/api/surveyRuns',
    638         ensureAuthenticated,
    639         ensureMIME(JSON_MIME),
    640         makeDocsGet('SurveyRun'));
    641     app.post('/api/surveyRuns',
    642         ensureAuthenticated,
    643         ensureMIME(JSON_MIME),
    644         function(req,res) {
    645             var doc = req.body;
    646             var hr;
    647             if ( !isDocPublished(doc.survey) ) {
    648                 hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
    649             } else {
    650                 hr = randomToken()
    651                 .handle({
    652                     201: function(token) {
    653                         doc.secret = token;
    654                         return postDocument('SurveyRun',doc);
    655                     }
    656                 });
    657             }
    658             hr.handle(res.send.bind(res));
    659         });
    660     app.get('/api/surveyRuns/:id',
    661         ensureAuthenticated,
    662         ensureMIME(JSON_MIME),
    663         makeDocGet_id('SurveyRun'));
    664     app.put('/api/surveyRuns/:id',
    665         ensureAuthenticated,
    666         ensureMIME(JSON_MIME),
    667         function (req,res) {
    668             var id = req.params.id;
    669             var doc = req.body;
    670             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    671             var hr;
    672             if ( !isDocPublished(doc.survey) ) {
    673                 hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
    674             } else if ( typeof doc.secret === 'undefined' ) {
    675                 hr = randomToken()
    676                 .handle({
    677                     201: function(token) {
    678                         doc.secret = token;
    679                         return putDocument(id,rev,'SurveyRun',doc);
    680                     }
    681                 });
    682             } else {
    683                 hr = putDocument(id,rev,'SurveyRun',doc);
    684             }
    685             hr.handle(res.send.bind(res));
    686         });
    687     app.delete('/api/surveyRuns/:id',
    688         ensureAuthenticated,
    689         ensureMIME(JSON_MIME),
    690         function(req,res) {
    691             var id = req.params.id;
    692             var doc = req.body;
    693             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    694             getResponsesBySurveyRunId(id)
    695             .handle({
    696                 200: function(responses) {
    697                     if ( responses.length > 0 ) {
    698                         return new HTTPResult(403,{error:"Cannot delete run that has responses."});
    699                     } else {
    700                         return deleteDocument(id,rev);
    701                     }
    702                 }
    703             }).handle(res.send.bind(res));
    704         });
    705     app.get('/api/surveyRuns/:id/responses',
    706         ensureAuthenticated,
    707         ensureMIME(JSON_MIME),
    708         function(req,res) {
    709             var id = req.params.id;
    710             getResponsesBySurveyRunId(id)
    711             .handle(res.send.bind(res));
    712         });
    713     app.get('/api/surveyRuns/:id/responses.csv',
    714         ensureAuthenticated,
    715         ensureMIME(CSV_MIME),
    716         function(req, res) {
    717             var id = req.params.id;
    718             getResponsesBySurveyRunId(id)
    719             .handle({
    720                 200: function(responses) {
    721                     var answers = _.map(responses,function(response){
    722                         return response.answers;
    723                     });
    724                     res.set({
    725                         'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
    726                     });
    727                     res.status(200);
    728                     writeObjectsToCSVStream(answers, res);
    729                     res.end();
    730                 },
    731                 default: res.send.bind(res)
    732             });
    733         });
    734    
    735     // Responses
    736     function getResponsesBySurveyRunId(surveyRunId) {
    737         var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
    738         return HTTPResult.fromResponsePromise(couch.get(url).response,
    739                                               handleUnknownError)
    740         .handle({
    741             200: handleRowValues,
    742             default: handleUnknownResponse
    743         });
    744     }
    745     app.get('/api/responses',
    746         ensureAuthenticated,
    747         ensureMIME(JSON_MIME),
    748         function(req,res){
    749             var hr;
    750             if ( 'surveyRunId' in req.query ) {
    751                 hr = getResponsesBySurveyRunId(req.query.surveyRunId);
    752             } else {
    753                 hr = getDocumentsOfType('Response');
    754             }
    755             hr.handle(res.send.bind(res));
    756         });
    757     app.get('/api/responses/:id',
    758         ensureAuthenticated,
    759         ensureMIME(JSON_MIME),
    760         makeDocGet_id('Response'));
    761     app.post('/api/responses',
    762         ensureAuthenticated,
    763         ensureMIME(JSON_MIME),
    764         function (req,res) {
    765             var doc = req.body;
    766             randomToken()
    767             .handle({
    768                 201: function(token) {
    769                     doc.secret = token;
    770                     return postDocument('Response',doc);
    771                 }
    772             })
    773             .handle(res.send.bind(res));
    774         });
    775     app.put('/api/responses/:id',
    776         ensureAuthenticated,
    777         ensureMIME(JSON_MIME),
    778         function (req,res) {
    779             var id = req.params.id;
    780             var doc = req.body;
    781             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    782             var hr;
    783             if ( typeof doc.secret === 'undefined' ) {
    784                 hr = randomToken()
    785                 .handle({
    786                     201: function(token) {
    787                         doc.secret = token;
    788                         return putDocument(id,rev,'Response',doc);
    789                     }
    790                 });
    791             } else {
    792                 hr = putDocument(id,rev,'Response',doc);
    793             }
    794             hr.handle(res.send.bind(res));
    795         });
    796     app.delete('/api/responses/:id',
    797         ensureAuthenticated,
    798         ensureMIME(JSON_MIME),
    799         makeDocDel_id('Response'));
    800 
    801     //respondents api
    802     function isSurveyRunRunning(surveyRun) {
    803         var now = new Date();
    804         var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
    805         var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
    806         return afterStart && beforeEnd;
    807     }
    808     app.get('/api/open/responses/:id',
    809         ensureMIME(JSON_MIME),
    810         function(req,res) {
    811             var id = req.params.id;
    812             var rev = etags.parse(req.header('If-Non-Match'))[0];
    813             var secret = req.query.secret;
    814             getDocument(id,rev,'Response')
    815             .handle({
    816                 200: function(response) {
    817                     if ( response.secret === secret ) {
    818                         return getDocument(response.surveyRunId,[],'SurveyRun')
    819                         .handle({
    820                             200: function(surveyRun) {
    821                                 if ( !isSurveyRunRunning(surveyRun) ) {
    822                                     return new HTTPResult(404,{error:"Survey is not running anymore."});
    823                                 } else {
    824                                     response._surveyRun = surveyRun;
    825                                     return response;
    826                                 }
    827                             }
    828                         });
    829                     } else {
    830                         return new HTTPResult(403,{error: "Wrong secret for response"});
    831                     }
    832                 }
    833             })
    834             .handle(res.send.bind(res));
    835         });
    836     app.post('/api/open/responses',
    837         ensureMIME(JSON_MIME),
    838         function(req,res) {
    839             var secret = req.query.secret;
    840             var response = req.body;
    841             delete response._surveyRun;
    842             getDocument(response.surveyRunId,[],'SurveyRun')
    843             .handle({
    844                 200: function(surveyRun) {
    845                     if ( surveyRun.secret !== secret ) {
    846                         return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
    847                     } else if ( !isSurveyRunRunning(surveyRun) ) {
    848                         return new HTTPResult(404,{error:"Survey is not running anymore."});
    849                     } else if ( surveyRun.mode === 'closed' ) {
    850                         return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
    851                     } else {
    852                         return randomToken()
    853                         .handle({
    854                             201: function(token) {
    855                                 response.secret = token;
    856                                 return postDocument('Response',response)
    857                                 .handle({
    858                                     201: function(doc){
    859                                         doc._surveyRun = surveyRun;
    860                                         return doc;
    861                                     }
    862                                 });
    863                             }
    864                         });
    865                     }
    866                 }
    867             })
    868             .handle(res.send.bind(res));
    869         });
    870     app.put('/api/open/responses/:id',
    871         ensureMIME(JSON_MIME),
    872         function(req,res){
    873             var id = req.params.id;
    874             var doc = req.body;
    875             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    876             var secret = req.query.secret || doc.secret;
    877             delete doc._surveyRun;
    878             getDocument(id,[],'Response')
    879             .handle({
    880                 200: function(prev) {
    881                     if ( prev.secret !== secret ) {
    882                         return new HTTPResult(403,{error: "Secrets are not the same."});
    883                     } else if ( prev.surveyRunId !== doc.surveyRunId ) {
    884                         return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
    885                     } else {
    886                         doc.secret = secret; // restore in case it got lost or was changed
    887                         return getDocument(doc.surveyRunId,[],'SurveyRun')
    888                         .handle({
    889                             200: function(surveyRun) {
    890                                 if ( !isSurveyRunRunning(surveyRun) ) {
    891                                     return new HTTPResult(404,{error:"Survey is not running anymore."});
    892                                 } else {
    893                                     return putDocument(id,rev,'Response',doc)
    894                                     .handle({
    895                                         200: function(doc) {
    896                                             doc._surveyRun = surveyRun;
    897                                             return new HTTPResult(201,doc);
    898                                         }
    899                                     });
    900                                 }
    901                             }
    902                         });
    903                     }
    904                 }
    905             })
    906             .handle(res.send.bind(res));
    907         });
    908     app.delete('/api/open/responses/:id',
    909         ensureMIME(JSON_MIME),
    910         function(req,res){
    911             var id = req.params.id;
    912             var doc = req.body;
    913             var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
    914             var secret = req.query.secret || doc.secret;
    915             delete doc._surveyRun;
    916             getDocument(id,[],'Response')
    917             .handle({
    918                 200: function(prev) {
    919                     if ( prev.secret !== secret ) {
    920                         return new HTTPResult(403,{error: "Secrets are not the same."});
    921                     } else {
    922                         return getDocument(doc.surveyRunId,[],'SurveyRun')
    923                         .handle({
    924                             200: function(surveyRun) {
    925                                 if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
    926                                     return deleteDocument(id,rev,doc);
    927                                 } else {
    928                                     return new HTTPResult(403,{error:"Not allowed to delete response."});
    929                                 }
    930                             }
    931                         });
    932                     }
    933                 }
    934             })
    935             .handle(res.send.bind(res));
    936         });
    937 
    938     // uuids
    939     app.get('/api/uuids',
    940         ensureAuthenticated,
    941         ensureMIME(JSON_MIME),
    942         function(req,res){
    943             var count = (req.query.count && parseInt(req.query.count,10)) || 1;
    944             HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
    945                                            handleUnknownError)
    946             .handle({
    947                 200: function(res) {
    948                     return res.uuids;
    949                 },
    950                 default: handleUnknownResponse
    951             })
    952             .handle(res.send.bind(res));
    953         });
     147    app.use('/api/open',
     148            require('./api/open')(couch,schema).app);
    954149
    955150    app.get('/api/mode',
    956         ensureMIME(JSON_MIME),
     151        apiUtil.ensureMIME(apiUtil.JSON_MIME),
    957152        function(req,res){
    958153            res.send({mode:env.mode});
     
    961156
    962157    return app;
    963 }
    964 
    965 function writeObjectsToCSVStream(objects, stream, prelude) {
    966     var keys = _.chain(objects)
    967                 .map(_.keys)
    968                 .flatten()
    969                 .uniq()
    970                 .value();
    971     var idxs = {};
    972     _.forEach(keys, function(key,idx){
    973         idxs[key] = idx;
    974     });
    975     var writer = new CSV.CsvWriter(stream);
    976     if ( prelude ) {
    977         _.forEach(prelude, function(val,key){
    978             writer.writeRecord([key,val]);
    979         });
    980     }
    981     writer.writeRecord(keys);
    982     _.forEach(objects, function(obj){
    983         var row = [];
    984         _.forEach(obj, function(val,key){
    985             row[idxs[key]] = val;
    986         });
    987         writer.writeRecord(row);
    988     });
    989 }
    990 
    991 function randomToken() {
    992     var result = new HTTPResult();
    993     cryptoken(8)
    994     .then(function(token){
    995         result.set(201,token);
    996     }, function(ex){
    997         result.set(500,{error:"Cannot generate secrets.", innerError: ex});
    998     });
    999     return result;
    1000158}
    1001159
  • Dev/trunk/src/server/bin/heroku.js

    r523 r525  
    66console.log("Using CouchDB on",env.couchServerURL+env.dbName);
    77
    8 require('../app').App(env)
     8require('../app').app(env)
    99.then(function(app){
    1010    app.listen(env.port, function() {
  • Dev/trunk/src/server/bin/upgrade-db-1-to-2.js

    r487 r525  
    11var env = require('../env')
     2  , HTTPResult = require('../util/http-result')
    23  , upgradeCouch = require('../config/upgrade-couchdb')
    34  , cryptoken = require('../util/crypto-token')
    4   , Q = require('q')
    55  ;
    66upgradeCouch(env.couchServerURL,env.dbName,function(doc){
    7     if ( (doc.type === 'Response' || doc.type === 'SurveyRun') && !doc.secret ) {
    8         return cryptoken(8).then(function(token){
     7    if ( (doc.type === 'Response' || doc.type === 'SurveyRun') &&
     8         !doc.secret ) {
     9        return cryptoken()
     10        .then(function(token){
    911            doc.secret = token;
    1012            return doc;
    1113        });
    1214    } else {
    13         return Q.reject();
     15        return new HTTPResult(-1);
    1416    }
    1517}).then(function(res){
  • Dev/trunk/src/server/bin/upgrade-db-2-to-3.js

    r506 r525  
    22  , upgradeCouch = require('../config/upgrade-couchdb')
    33  , cryptoken = require('../util/crypto-token')
    4   , Q = require('q')
     4  , HTTPResult = require('../util/http-result')
    55  , _ = require('underscore')
    66  ;
     
    5151        return doc;
    5252    } else {
    53         return Q.reject();
     53        return new HTTPResult(-1);
    5454    }
    5555}).then(function(res){
  • Dev/trunk/src/server/bin/upgrade-db-3-to-4.js

    r508 r525  
    22  , upgradeCouch = require('../config/upgrade-couchdb')
    33  , cryptoken = require('../util/crypto-token')
    4   , Q = require('q')
     4  , HTTPResult = require('../util/http-result')
    55  , _ = require('underscore')
    66  ;
     
    2525        return doc;
    2626    } else {
    27         return Q.reject();
     27        return HTTPResult.fail();
    2828    }
    2929}).then(function(res){
  • Dev/trunk/src/server/config/check-couchdb.js

    r492 r525  
    1 var Q = require('q')
    2   , _ = require('underscore')
     1var _ = require('underscore')
     2  , HTTPResult = require('../util/http-result')
    33  , validator = require('../util/validator')
    4   , CouchDB = require('../util/couch').CouchDB
     4  , CouchDB = require('../util/couch')
     5  , objF = require('../util/object')
    56  ;
    67
     
    1011    var server = new CouchDB(couchServerURL,dbName);
    1112    var designRe = /^_design\//;
    12 
    13     var codes = {};
    14     function codeUnique(code) {
    15         if ( code in codes ) {
    16             return false;
    17         } else {
    18             codes[code] = true;
    19             return true;
    20         }
    21     }
    2213
    2314    return server.get('/_all_docs')
     
    3223                .then(function(doc){
    3324                    var valid = validator(doc,schema);
    34                     if ( doc.type === "Question" && !codeUnique(doc.code) ) {
    35                         valid.valid = false;
    36                         valid.error = "Question code "+doc.code+" is not unique.";
    37                     }
    3825                    result[doc._id] = valid;
    3926                    return result;
    4027                });
    4128            });
    42         }, Q.resolve({}))
     29        }, new HTTPResult(200,{}))
    4330        .value();
    44     }).all();
     31    }).then(function(result){
     32        return server.get('_design/questions/_view/all_variables')
     33        .then(function(variables){
     34            var dups = objF.findDuplicates(
     35                variables.rows,
     36                function(row){ return row.key; });
     37            result._duplicateVariables = _.groupBy(
     38                dups,
     39                function(dup){ return dup.key; });
     40            return result;
     41        });
     42    });
    4543};
  • Dev/trunk/src/server/config/config-couchdb.js

    r519 r525  
    1 var Q = require('q')
     1var HTTPResult = require('../util/http-result')
    22  , _ = require('underscore')
    3   , CouchDB = require('../util/couch').CouchDB
     3  , CouchDB = require('../util/couch')
    44  , util = require('util')
    55  ;
     
    6262                }
    6363            });
    64         }, Q.resolve());
     64        }, new HTTPResult(200));
    6565    });
    6666};
  • Dev/trunk/src/server/config/couchdb-design-docs.js

    r523 r525  
     1var fs = require('fs')
     2  , path = require('path')
     3  ;
     4
     5function readModule(name) {
     6    var filename;
     7    if ( name.indexOf('.') === 0 ) {
     8        filename = path.join(path.dirname(module.filename),name+'.js');
     9    } else {
     10        var dirname = path.join(path.dirname(module.filename),
     11                                '../../node_modules',name);
     12        filename = path.join(dirname,name+'.js');
     13        var pkgfile = path.join(dirname,'package.json');
     14        if ( fs.exists(pkgfile) ) {
     15            var pkg = require(pkgfile);
     16            if ( pkg.main ) {
     17                filename = path.join(dirname,pkg.main);
     18            }
     19        }
     20    }
     21    var src = fs.readFileSync(filename, 'utf8');
     22    src = src.replace(/require\s*\(\s*(['\"])([^'"]*)(['\"])\s*\)/g,
     23                      'require($1views/lib/$2$3)');
     24    return src;
     25}
     26
    127module.exports = {
    228
     
    80106                reduce: function(key, values, rereduce) { return sum(values); }
    81107            },
     108            all_variables: {
     109                map: function(doc) {
     110                    if ( doc.type !== 'Question' ) { return; }
     111                    var _ = require('views/lib/underscore');
     112                    var objF = require('views/lib/object');
     113                    _.chain(objF.collectFields('subcode',doc.content))
     114                    .map(function(subcode){
     115                        return doc.code+subcode;
     116                    })
     117                    .each(function(variable){
     118                        emit(variable,doc._id);
     119                    });
     120                }
     121            },
     122            published_variables: {
     123                map: function(doc) {
     124                    if ( doc.type !== 'Question' || !doc.publicationDate ) { return; }
     125                    var _ = require('views/lib/underscore');
     126                    var objF = require('views/lib/object');
     127                    _.chain(objF.collectFields('subcode',doc.content))
     128                    .map(function(subcode){
     129                        return doc.code+subcode;
     130                    })
     131                    .each(function(variable){
     132                        emit(variable,doc._id);
     133                    });
     134                }
     135            },
    82136            all_by_code: {
    83137                map: function(doc){
     
    91145                    emit(doc.code,doc);
    92146                }
     147            },
     148            lib: {
     149                underscore: readModule('underscore'),
     150                object: readModule('../util/object')
    93151            }
    94152        }
  • Dev/trunk/src/server/config/couchdb-schema.json

    r523 r525  
    1111    "nonEmptyString": { "type": "string", "minLength": 1 },
    1212    "codeString": { "type": "string", "pattern": "^[A-Za-z0-9]+$" },
     13    "subcodeString": { "type": "string", "pattern": "^[A-Za-z0-9]*$" },
    1314    "schemaInfo": {
    1415      "type": "object",
     
    148149        "properties": {
    149150          "type": { "type": "string", "pattern": "^StringInput$" },
    150           "subcode": { "$ref": "#/definitions/codeString" },
     151          "subcode": { "$ref": "#/definitions/subcodeString" },
    151152          "text": { "$ref": "#/definitions/nonEmptyString" }
    152153        },
     
    159160          "type": { "type": "string", "pattern": "^TextInput$" },
    160161          "maxLength": { "type": "integer" },
    161           "subcode": { "$ref": "#/definitions/codeString" },
     162          "subcode": { "$ref": "#/definitions/subcodeString" },
    162163          "text": { "$ref": "#/definitions/nonEmptyString" }
    163164        },
     
    172173          "max": { "type": "integer" },
    173174          "places": { "type": "integer" },
    174           "subcode": { "$ref": "#/definitions/codeString" },
     175          "subcode": { "$ref": "#/definitions/subcodeString" },
    175176          "text": { "$ref": "#/definitions/nonEmptyString" }
    176177        },
     
    192193              "minLabel": { "$ref": "#/definitions/nonEmptyString" },
    193194              "maxLabel": { "$ref": "#/definitions/nonEmptyString" },
    194               "subcode": { "$ref": "#/definitions/codeString" },
     195              "subcode": { "$ref": "#/definitions/subcodeString" },
    195196              "text": { "$ref": "#/definitions/nonEmptyString" }
    196197            },
     
    218219              "type": "object",
    219220              "properties": {
    220                   "subcode": { "$ref": "#/definitions/codeString" }
     221                  "subcode": { "$ref": "#/definitions/subcodeString" }
    221222              },
    222223              "required": ["subcode"],
    223224              "additionalProperties": false
    224225          },
    225           "subcode": { "$ref": "#/definitions/codeString" }
     226          "subcode": { "$ref": "#/definitions/subcodeString" }
    226227        },
    227228        "required":["type","items","subcode"],
     
    235236              "type": "object",
    236237              "properties": {
    237                 "subcode": { "$ref": "#/definitions/codeString" },
     238                "subcode": { "$ref": "#/definitions/subcodeString" },
    238239                "text": { "$ref": "#/definitions/nonEmptyString" }
    239240              },
     
    244245              "type": "object",
    245246              "properties": {
    246                 "subcode": { "$ref": "#/definitions/codeString" }
     247                "subcode": { "$ref": "#/definitions/subcodeString" }
    247248              },
    248249              "required": ["subcode"],
  • Dev/trunk/src/server/config/upgrade-couchdb.js

    r492 r525  
    1 var Q = require('q')
     1var HTTPResult = require('../util/http-result')
    22  , _ = require('underscore')
    3   , CouchDB = require('../util/couch').CouchDB
     3  , CouchDB = require('../util/couch')
    44  ;
    55
     
    1919                return server.get(doc.id)
    2020                .then(function(doc){
    21                     Q.when(upgrade(doc))
     21                    HTTPResult.when(200,upgrade(doc))
    2222                    .then(function(newDoc){
    2323                        return server.put(doc._id,newDoc)
     
    3232                });
    3333            });
    34         }, Q.resolve({}))
     34        }, new HTTPResult(200,{}))
    3535        .value();
    3636    }).all();
  • Dev/trunk/src/server/util/couch.js

    r522 r525  
    1 var CouchDB, _ref,
    2   __hasProp = {}.hasOwnProperty;
    3 
    4 var request = require('./q-request')
     1var request = require('./request')
    52  , _ = require('underscore')
    6   , Q = require('q');
     3  ;
    74
    85function CouchDB(url, db) {
     
    5855        if (opts.headers) { _.extend(options.headers, opts.headers); }
    5956    }
    60     var req = request(url, options);
    61     var res = req.response
    62     .then(function(res) {
    63         return req.then(function(res) {
    64             return JSON.parse(res);
    65         }, function(err) {
    66             return Q.reject(JSON.parse(err));
     57    return request(url, options)
     58        .handle({
     59            '-1': _.identity,
     60            default: function(status,result) { return JSON.parse(result); }
    6761        });
    68     }, function(err) {
    69         return Q.reject(err);
    70     });
    71     res.response = req.response
    72     .then(function(res) {
    73         return {
    74             statusCode: res.statusCode,
    75             headers: res.headers,
    76             body: JSON.parse(res.body)
    77         };
    78     }, function(err) {
    79         return Q.reject({error:err.message});
    80     });
    81     return res;
    8262}
    8363
     
    10888}
    10989
    110 exports.CouchDB = CouchDB;
     90module.exports = CouchDB;
  • Dev/trunk/src/server/util/crypto-token.js

    r487 r525  
    1 var Q = require('q')
     1var HTTPResult = require('./http-result')
    22  , crypto = require('crypto');
    33
    44module.exports = function(bytes) {
    5     var q = Q.defer();
    6     crypto.randomBytes(bytes || 8, function(ex, buf) {
    7         if ( ex ) {
    8             q.reject(ex);
    9         } else {
    10             q.resolve(buf.toString('hex'));
    11         }
    12     });
    13     return q.promise;
     5    bytes  =bytes || 8;
     6    var result = new HTTPResult();
     7    crypto.randomBytes(bytes, result.asCallback(201));
     8    return result
     9        .then(function(buf){
     10            return buf.toString('hex');
     11        });
    1412};
  • Dev/trunk/src/server/util/http-result.js

    r487 r525  
    1 // <init> :: status? -> result? -> HTTPResult
     1var _ = require('underscore')
     2  ;
     3
     4var when;
     5
     6// type Callback = status:Number -> result:Any -> Either HTTPResult Any
     7// type Handler = { f:Callback, next:HTTPResult }
     8
     9// <init> :: status:Number? -> result:Any? -> HTTPResult
     10var counter = 0;
    211function HTTPResult(status,result) {
     12    this.id = counter++;
    313    this.status = 0;
    414    this.result = null;
     
    919}
    1020
    11 HTTPResult.fromResponsePromise = function(promise,errorHandler) {
    12     var result = new HTTPResult();
    13     promise.then(function(response){
    14         result.set(response.statusCode,response.body);
    15     }, errorHandler && function(error){
    16         var ret = errorHandler(error);
    17         if ( ret.constructor === HTTPResult ) {
    18             ret.handle(result.set.bind(result));
    19         } else {
    20             result.set(500,ret);
    21         }
    22     });
    23     return result;
    24 };
    25 
    2621// isSet :: Bool
    2722HTTPResult.prototype.isSet = function() {
     
    2924};
    3025
    31 // set :: status -> result? -> ()
     26// set :: status:Number -> result:Any? -> ()
    3227HTTPResult.prototype.set = function(status,result) {
    33     if ( this.isSet() ) { throw new Error("Result was already set."); }
    34     if ( typeof status !== 'number' ) { throw new Error("Status must be a number."); }
     28    if ( this.isSet() ) {
     29        throw new Error("Result was already set.");
     30    }
     31    if ( !_.isNumber(status) ) {
     32        throw new Error("Status must be a number.");
     33    }
    3534    this.status = status;
    3635    this.result = result;
    37     for ( var i = 0; i < this._handlers.length; i++ ) {
    38         this._fire(this._handlers[i]);
     36    _.each(this._handlers,this._fire.bind(this));
     37};
     38
     39// _fire :: Handler -> ()
     40HTTPResult.prototype._fire = function(handler) {
     41    var ret = handler.f(this.status,this.result);
     42    if ( ret instanceof HTTPResult ) {
     43        ret.handle(handler.next.set.bind(handler.next));
     44    } else {
     45        handler.next.set(this.status,ret);
    3946    }
    4047};
    4148
    42 // _fire :: (status -> result -> Either HTTPResult Any) -> ()
    43 HTTPResult.prototype._fire = function(f) {
    44     var ret = f(this.status,this.result);
    45     if ( ret.constructor === HTTPResult ) {
    46         ret.handle(f._next.set.bind(f._next));
    47     } else {
    48         f._next.set(this.status,ret);
    49     }
    50 };
    51 
    52 /* handle :: Either (status -> result -> Either HTTPResult Any)
    53  *                  {number: (result -> Either HTTPResult Any),
    54  *                   'default': (status -> result -> Either HTTPResult Any)}
     49/* handle :: Either Callback
     50 *                  {status:Number*: (result -> Either HTTPResult Any),
     51 *                   'default'?: Callback}
    5552 *           -> HTTPResult
    5653 */
    5754HTTPResult.prototype.handle = function(fOrObj) {
    58     var f = typeof fOrObj === 'function' ?
     55    var f = _.isFunction(fOrObj) ?
    5956            fOrObj :
    6057            function(status,result) {
     
    6764                }
    6865            };
    69     f._next = new HTTPResult();
     66    var next = new HTTPResult();
     67    var handler = {
     68        f: f,
     69        next: next
     70    };
    7071    if ( this.isSet() ) {
    71         this._fire(f);
     72        this._fire(handler);
    7273    } else {
    73         this._handlers.push(f);
     74        this._handlers.push(handler);
    7475    }
    75     return f._next;
     76    return next;
     77};
     78
     79// then :: onSuccess:(result:Any -> status:Number)? ->
     80//         onError:(result:Any -> status:Number)? -> HTTPResult
     81HTTPResult.prototype.then = function(onSuccess,onError) {
     82    var f = function(status,result) {
     83        if ( status >= 200 && status < 300 && onSuccess ) {
     84            if ( onSuccess ) {
     85                result = onSuccess(result,status);
     86            }
     87        } else {
     88            if ( onError ) {
     89                result = onError(result,status);
     90            }
     91        }
     92        return result;
     93    };
     94    return this.handle(f);
     95};
     96
     97HTTPResult.prototype.asCallback = function(status) {
     98    status = status || 200;
     99    return function(ex,result) {
     100        if ( ex ) {
     101            this.set(-1,ex);
     102        } else {
     103            this.set(status,result);
     104        }
     105    }.bind(this);
     106};
     107
     108// when :: Either HTTPResult Any -> HTTPResult
     109when = HTTPResult.when = function(status,valueOrResult) {
     110    status = status || 200;
     111    if ( valueOrResult instanceof HTTPResult ) {
     112        return valueOrResult;
     113    } else {
     114        return new HTTPResult(status,valueOrResult);
     115    }
     116};
     117
     118// ok :: result:Any? -> HTTPResult
     119HTTPResult.ok = function(result) {
     120    return new HTTPResult(200,result);
     121};
     122
     123// fail :: error:Any? -> HTTPResult
     124HTTPResult.fail = function(error) {
     125    return new HTTPResult(-1,error);
    76126};
    77127
Note: See TracChangeset for help on using the changeset viewer.