source: Dev/trunk/src/server/app.js @ 499

Last change on this file since 499 was 499, checked in by hendrikvanantwerpen, 11 years ago
  • Schema patterns should be anchored.
  • App returns validation result on error.
File size: 34.1 KB
Line 
1var express = require("express")
2  , passport = require("passport")
3  , passportLocal = require("passport-local")
4  , fs = require("fs")
5  , path = require("path")
6  , proxy = require("./util/simple-http-proxy")
7  , CSV = require("ya-csv")
8  , CouchDB = require('./util/couch').CouchDB
9  , _ = 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  ;
15
16function assertSetting(name, settings, validate) {
17    if ( typeof settings[name] === 'undefined' ) {
18        throw new Error("Required setting '"+name+"' undefined.");
19    }
20    if ( _.isFunction(validate) && !validate(settings[name]) ) {
21        throw new Error("Setting '"+name+"' with value '"+settings[name]+"' is invalid.");
22    }
23}
24
25exports.App = function(settings) {
26
27    assertSetting("couchServerURL", settings, _.isString);
28    assertSetting("dbName", settings, _.isString);
29    var couch = new CouchDB(settings.couchServerURL,settings.dbName);
30   
31    var schema = require("./config/couchdb-schema.json");
32    return couch.get("schemaInfo").then(function(schemaInfo){
33        if (schemaInfo.version !== schema.version) {
34            var msg =  "Database has version "+schemaInfo.version+" but we expect version "+schema.version;
35            throw new Error(msg);
36        }
37        return configureApp(settings,couch,schema);
38    });
39
40};
41
42function configureApp(settings,couch,schema) {
43
44    function clientPath(relativePath) {
45        return path.resolve(__dirname+'/../client/'+relativePath);
46    }
47
48    passport.use(new passportLocal.Strategy(function(username, password, done){
49        if ( username === "igor" && password === "mayer" ) {
50            done(null,{ username: "igor" });
51        } else {
52            done(null,false,{ message: 'Invalid credentials.' });
53        }
54    }));
55    passport.serializeUser(function(user, done) {
56        done(null, user.username);
57    });
58    passport.deserializeUser(function(id, done) {
59        done(null, {username: id});
60    });
61
62    var app = express();
63    app.use(express.logger());
64    app.use(express.compress());
65    app.use(express.favicon());
66
67    // cookies and session
68    app.use(express.cookieParser());
69    app.use(express.session({ secret: "quasi experimental design" }));
70    app.use('/api',express.bodyParser());
71
72    // passport
73    app.use(passport.initialize());
74    app.use(passport.session());
75   
76    // various middlewares
77    function ensureAuthenticated(req,res,next){
78        if (!req.user) {
79            return res.send(401,{error:"Login before accessing API."});
80        } else {
81            return next();
82        }
83    }
84    function returnUser(req,res) {
85        res.send(200, req.user);
86    }
87    function notImplemented(req,res) {
88        res.send(501,{error:"API not implemented yet."});
89    }
90
91    // static resources
92    app.get('/', function(request, response){
93        response.sendfile(clientPath('index.html'));
94    });
95    app.get('/*.html', function(request, response) {
96        response.sendfile(clientPath(request.path));
97    });
98    _.each(['/dojo', '/dijit', '/dojox', '/qed', '/qed-client'], function(dir){
99        app.use(dir, express.static(clientPath(dir)));
100    });
101
102    // post to this url to login
103    app.post(
104        '/api/login',
105        passport.authenticate('local'),
106        returnUser);
107
108    // return the info for the current logged in user
109    app.get(
110        '/api/login',
111        ensureAuthenticated,
112        returnUser);
113
114    // explicitly logout this user
115    app.post(
116        '/api/logout',
117        ensureAuthenticated,
118        function(req,res){
119            req.logout();
120            res.send(200,{});
121        });
122
123    var JSON_MIME = 'application/json';
124    var CSV_MIME = 'application/json';
125    function ensureMIME(mimeType) {
126        return function(req,res,next) {
127            if (!req.accepts(mimeType)) {
128                res.send(406);
129            } else {
130                res.set({
131                    'Content-Type': mimeType
132                });
133                next();
134            }
135        };
136    }
137   
138    function stripAndReturnPrivates(obj) {
139        var priv = {};
140        _.each(obj||{},function(val,key){
141            if (key.substring(0,1) === '_') {
142                priv[key] = val;
143                delete obj[key];
144            }
145        });
146        return priv;
147    }
148
149    function identity(obj) { return obj; }
150    function handleUnknownResponse(status,error) {
151        return new HTTPResult(500,{error: error.reason});
152    }
153    function handleUnknownError(error) {
154        return new HTTPResult(500, {error: "Unknown error", innerError: error});
155    }
156    function handleRowValues(result) {
157        return _.map(result.rows, function(item) { return item.value; });
158    }
159    function handleRowDocs(result) {
160        return _.map(result.rows, function(item) { return item.doc; });
161    }
162    function handleRowFirstDoc(result) {
163        if ( result.rows.length > 0 ) {
164            return result.rows[0].doc;
165        } else {
166            return new HTTPResult(404,{error:"No document found."});
167        }
168    }
169    function handleRowFirstValue(result) {
170        if ( result.rows.length > 0 ) {
171            return result.rows[0].value;
172        } else {
173            return new HTTPResult(404,{error:"No document found."});
174        }
175    }
176    function handleRowDictOfDocs(result) {
177        return _.reduce(result.rows, function(dict,item) {
178            dict[item.key] = item.doc;
179            return dict;
180        }, {});
181    }
182    function handleRowDictOfValues(result) {
183        return _.reduce(result.rows, function(dict,item) {
184            dict[item.key] = item.value;
185            return dict;
186        }, {});
187    }
188
189    function getDocumentsOfType (type) {
190        var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
191        return HTTPResult.fromResponsePromise(couch.get(url).response,
192                                              handleUnknownError)
193        .handle({
194            200: handleRowValues,
195            404: function() { return {error: "Cannot find collection of type "+type}; },
196            default: handleUnknownResponse
197        });
198    }
199    function getDocument(id,rev,type) {
200        var opts = {headers:{}};
201        if (rev) {
202            opts.headers['If-Non-Match'] = '"'+rev+'"';
203        }
204        return HTTPResult.fromResponsePromise(couch.get(id,opts).response,
205                                              handleUnknownError)
206        .handle({
207            200: function(doc){
208                if ( doc.type !== type ) {
209                    return new HTTPResult(404,{error:"Document not found."});
210                } else {
211                    var priv = stripAndReturnPrivates(doc);
212                    if ( priv._rev !== rev ) {
213                        doc._id = priv._id;
214                        doc._rev = priv._rev;
215                        return doc;
216                    } else {
217                        return new HTTPResult(304);
218                    }
219                }
220            },
221            304: identity,
222            default: handleUnknownResponse
223        });
224    }
225    function putDocument(id,rev,type,doc) {
226        var priv = stripAndReturnPrivates(doc);
227        var valid;
228        if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
229            var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
230            return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response,
231                                                  handleUnknownError)
232            .handle({
233                201: function(res){
234                    doc._id = res.id;
235                    doc._rev = res.rev;
236                    return doc;
237                },
238                409: function(error) {
239                    return {error: error.reason};
240                },
241                default: handleUnknownResponse
242            });
243        } else {
244            return new HTTPResult(400,{error: "Document failed schema verification.", valid: valid});
245        }
246    }
247    function deleteDocument(id,rev) {
248        if ( rev ) {
249            var opts = {headers:{'If-Match':'"'+rev+'"'}};
250            return HTTPResult.fromResponsePromise(couch.delete(id,opts).response,
251                                                  handleUnknownError)
252            .handle({
253                200: identity,
254                409: function(error) {
255                    return {error: error.reason};
256                },
257                default: handleUnknownResponse
258            });
259        } else {
260            return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
261        }
262    }
263    function postDocument(type,doc) {
264        var priv = stripAndReturnPrivates(doc);
265        var valid;
266        if ( doc.type === type && (valid = validator(doc, schema)).valid ) {
267            return HTTPResult.fromResponsePromise(couch.post(doc).response,
268                                                  handleUnknownError)
269            .handle({
270                201: function(response) {
271                    doc._id = response.id;
272                    doc._rev = response.rev;
273                    return doc;
274                },
275                default: handleUnknownResponse
276            });
277        } else {
278            return new HTTPResult(400,{error: "Document failed schema verification.", valid: valid});
279        }
280    }
281
282    function makeDocsGet(type) {
283        return function(req,res) {
284            getDocumentsOfType(type)
285            .handle(res.send.bind(res));
286        };
287    }
288    function makeDocGet_id(type) {
289        return function(req,res) {
290            var id = req.params.id;
291            var rev = etags.parse(req.header('If-Non-Match'))[0];
292            getDocument(id,rev,type)
293            .handle({
294                200: function(doc){
295                    res.set({
296                        'ETag': etags.format([doc._rev])
297                    }).send(200, doc);
298                },
299                default: res.send.bind(res)
300            });
301        };
302    }
303    function makeDocPut_id(type) {
304        return function(req,res) {
305            var id = req.params.id;
306            var doc = req.body;
307            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
308            putDocument(id,rev,type,doc)
309            .handle({
310                201: function(doc) {
311                    res.set({
312                        'ETag': etags.format([doc._rev])
313                    }).send(201, doc);
314                },
315                default: res.send.bind(res)
316            });
317        };
318    }
319    function makeDocDel_id(type) {
320        return function(req,res) {
321            var id = req.params.id;
322            var doc = req.body;
323            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
324            deleteDocument(id,rev)
325            .handle(res.send.bind(res));
326        };
327    }
328    function makeDocPost(type) {
329        return function(req,res) {
330            var doc = req.body;
331            postDocument(type,doc)
332            .handle({
333                201: function(doc) {
334                    res.set({
335                        'ETag': etags.format([doc._rev])
336                    }).send(201, doc);
337                },
338                default: res.send.bind(res)
339            });
340        };
341    }
342
343    // Questions
344    function getQuestionsWithCode(code) {
345        var url = '_design/questions/_view/by_code';
346        var query = {include_docs:true,key:code};
347        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
348                                              handleUnknownError)
349        .handle({
350            200: handleRowDocs,
351            default: handleUnknownResponse
352        });
353    }
354    function getQuestionsAndCodes() {
355        var url = '_design/questions/_view/by_code';
356        var query = {include_docs:true};
357        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
358                                              handleUnknownError)
359        .handle({
360            200: handleRowDictOfDocs,
361            default: handleUnknownResponse
362        });
363    }
364    function getQuestionsWithTopic(topic) {
365        var url = '_design/questions/_view/all_topics';
366        var query = {reduce:false,include_docs:true,key:topic};
367        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
368                                              handleUnknownError)
369        .handle({
370            200: handleRowDocs,
371            default: handleUnknownResponse
372        });
373    }
374    function getQuestionsWithCategoryAndTopic(category,topic) {
375        var hasTopic = typeof topic !== 'undefined';
376        var url = '_design/questions/_view/all';
377        var query = {reduce:false,include_docs:true,
378                     startkey:hasTopic?[category,topic]:[category],
379                     endkey:hasTopic?[category,topic]:[category,{}]};
380        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
381                                              handleUnknownError)
382        .handle({
383            200: handleRowDocs,
384            default: handleUnknownResponse
385        });
386    }
387    app.get('/api/questions',
388        ensureAuthenticated,
389        ensureMIME(JSON_MIME),
390        function(req,res) {
391            var hr;
392            if ( 'category' in req.query ) {
393                hr = getQuestionsWithCategoryAndTopic(req.query.category,req.query.topic);
394            } else if ( 'topic' in req.query ) {
395                hr = getQuestionsWithTopic(req.query.topic);
396            } else if ( 'code' in req.query ) {
397                hr = getQuestionsWithCode(req.query.code);
398            }
399            hr.handle(res.send.bind(res));
400        });
401    app.post('/api/questions',
402        ensureAuthenticated,
403        ensureMIME(JSON_MIME),
404        function (req,res) {
405            var doc = req.body;
406            getQuestionsWithCode(doc.code)
407            .handle({
408                200: function(others) {
409                    if ( others.length > 0 ) {
410                        return new HTTPResult(403,{error:"Other question with this code already exists."});
411                    } else {
412                        return postDocument('Question',doc);
413                    }
414                }
415            })
416            .handle(res.send.bind(res));
417        });
418    app.get('/api/questions/:id',
419        ensureAuthenticated,
420        ensureMIME(JSON_MIME),
421        makeDocGet_id('Question'));
422    app.put('/api/questions/:id',
423        ensureAuthenticated,
424        ensureMIME(JSON_MIME),
425        function (req,res) {
426            var id = req.params.id;
427            var doc = req.body;
428            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
429            getQuestionsWithCode(doc.code)
430            .handle({
431                200: function(others) {
432                    if ( others.length > 0 && _.some(others,function(other){ return other._id !== id; })  ) {
433                        return new HTTPResult(403,{error:"Other question with this code already exists."});
434                    } else {
435                        return putDocument(id,rev,'Question',doc);
436                    }
437                }
438            })
439            .handle(res.send.bind(res));
440        });
441    app.delete('/api/questions/:id',
442        ensureAuthenticated,
443        ensureMIME(JSON_MIME),
444        makeDocDel_id('Question'));
445
446
447    // Categories and topics
448    function getTopicsWithCategory(category) {
449        var url = '_design/questions/_view/all';
450        return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
451                                       handleUnknownError)
452        .handle({
453            200: function(result) {
454                return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
455            },
456            default: handleUnknownResponse
457        });
458    }
459    app.get('/api/categories',
460        ensureAuthenticated,
461        ensureMIME(JSON_MIME),
462        function(req,res) {
463            var url = '_design/questions/_view/all';
464            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
465                                           handleUnknownError)
466            .handle({
467                200: function(result) {
468                    return _.map(result.rows, function(item) {
469                        return { name:item.key[0], count:item.value };
470                    });
471                },
472                default: handleUnknownResponse
473            })
474            .handle(res.send.bind(res));
475        });
476    app.get('/api/categories/:category/topics',
477        ensureAuthenticated,
478        ensureMIME(JSON_MIME),
479        function(req,res) {
480            var category = req.params.category;
481            getTopicsWithCategory(category)
482            .handle(res.send.bind(res));
483        });
484    app.get('/api/topics',
485        ensureAuthenticated,
486        ensureMIME(JSON_MIME),
487        function(req,res) {
488            var url = '_design/questions/_view/all_topics';
489            var hr;
490            if ( 'category' in req.query ) {
491                hr = getTopicsWithCategory(req.query.category);
492            } else {
493                hr = HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
494                                                    handleUnknownError)
495                .handle({
496                    200: function(result) {
497                        return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
498                    },
499                    default: handleUnknownResponse
500                });
501            }
502            hr.handle(res.send.bind(res));
503        });
504
505    // Surveys
506    function areSurveyQuestionsUnique(survey) {
507        return _.chain(survey.questions)
508                .map(function(q){ return q.code; })
509                .uniq()
510                .value().length === survey.questions.length;
511    }
512    app.get('/api/surveys',
513        ensureAuthenticated,
514        ensureMIME(JSON_MIME),
515        function(req,res) {
516            var url;
517            if ( 'drafts' in req.query ) {
518                url = '_design/surveys/_view/drafts';
519            } else if ( 'published' in req.query ) {
520                url = '_design/surveys/_view/published';
521            } else {
522                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
523            }
524            HTTPResult.fromResponsePromise(couch.get(url).response,
525                                           handleUnknownError)
526            .handle({
527                200: function(result) {
528                    return _.map(result.rows, function(item) { return item.value; });
529                },
530                default: handleUnknownResponse
531            })
532            .handle(res.send.bind(res));
533        });
534    app.post('/api/surveys',
535        ensureAuthenticated,
536        ensureMIME(JSON_MIME),
537        function(req,res) {
538            var doc = req.body;
539            var hr;
540            if ( !areSurveyQuestionsUnique(doc) ) {
541                hr = new HTTPResult(400,{error:"Survey contains duplicate questions."});
542            } else {
543                hr = postDocument('Survey',doc);
544            }
545            hr.handle(res.send.bind(res));
546        });
547    app.get('/api/surveys/:id',
548        ensureAuthenticated,
549        ensureMIME(JSON_MIME),
550        makeDocGet_id('Survey'));
551    app.put('/api/surveys/:id',
552        ensureAuthenticated,
553        ensureMIME(JSON_MIME),
554        function(req,res) {
555            var id = req.params.id;
556            var doc = req.body;
557            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
558            var hr;
559            if ( !areSurveyQuestionsUnique(doc) ) {
560                new HTTPResult(400,{error:"Survey contains duplicate questions."})
561                .handle(res.send.bind(res));
562            } else {
563                putDocument(id,rev,'Survey',doc)
564                .handle({
565                    201: function(doc) {
566                        res.set({
567                            'ETag': etags.format([doc._rev])
568                        }).send(201, doc);
569                    },
570                    default: res.send.bind(res)
571                });
572            }
573        });
574    app.delete('/api/surveys/:id',
575        ensureAuthenticated,
576        ensureMIME(JSON_MIME),
577        makeDocDel_id('Survey'));
578
579    // SurveyRuns
580    app.get('/api/surveyRuns',
581        ensureAuthenticated,
582        ensureMIME(JSON_MIME),
583        makeDocsGet('SurveyRun'));
584    app.post('/api/surveyRuns',
585        ensureAuthenticated,
586        ensureMIME(JSON_MIME),
587        function(req,res) {
588            var doc = req.body;
589            randomToken()
590            .handle({
591                201: function(token) {
592                    doc.secret = token;
593                    return postDocument('SurveyRun',doc);
594                }
595            })
596            .handle(res.send.bind(res));
597        });
598    app.get('/api/surveyRuns/:id',
599        ensureAuthenticated,
600        ensureMIME(JSON_MIME),
601        makeDocGet_id('SurveyRun'));
602    app.put('/api/surveyRuns/:id',
603        ensureAuthenticated,
604        ensureMIME(JSON_MIME),
605        function (req,res) {
606            var id = req.params.id;
607            var doc = req.body;
608            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
609            var hr;
610            if ( typeof doc.secret === 'undefined' ) {
611                hr = randomToken()
612                .handle({
613                    201: function(token) {
614                        doc.secret = token;
615                        return putDocument(id,rev,'SurveyRun',doc);
616                    }
617                });
618            } else {
619                hr = putDocument(id,rev,'SurveyRun',doc);
620            }
621            hr.handle(res.send.bind(res));
622        });
623    app.delete('/api/surveyRuns/:id',
624        ensureAuthenticated,
625        ensureMIME(JSON_MIME),
626        function(req,res) {
627            var id = req.params.id;
628            var doc = req.body;
629            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
630            getResponsesBySurveyRunId(id)
631            .handle({
632                200: function(responses) {
633                    if ( responses.length > 0 ) {
634                        return new HTTPResult(403,{error:"Cannot delete run that has responses."});
635                    } else {
636                        return deleteDocument(id,rev);
637                    }
638                }
639            }).handle(res.send.bind(res));
640        });
641    app.get('/api/surveyRuns/:id/responses',
642        ensureAuthenticated,
643        ensureMIME(JSON_MIME),
644        function(req,res) {
645            var id = req.params.id;
646            getResponsesBySurveyRunId(id)
647            .handle(res.send.bind(res));
648        });
649    app.get('/api/surveyRuns/:id/responses.csv',
650        ensureAuthenticated,
651        ensureMIME(CSV_MIME),
652        function(req, res) {
653            var id = req.params.id;
654            getResponsesBySurveyRunId(id)
655            .handle({
656                200: function(responses) {
657                    var flatResponses = responsesToVariables(responses);
658                    res.set({
659                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
660                    });
661                    res.status(200);
662                    writeObjectsToCSVStream(flatResponses, res);
663                    res.end();
664                },
665                default: res.send.bind(res)
666            });
667        });
668   
669    // Responses
670    function getResponsesBySurveyRunId(surveyRunId) {
671        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
672        return HTTPResult.fromResponsePromise(couch.get(url).response,
673                                              handleUnknownError)
674        .handle({
675            200: handleRowValues,
676            default: handleUnknownResponse
677        });
678    }
679    app.get('/api/responses',
680        ensureAuthenticated,
681        ensureMIME(JSON_MIME),
682        function(req,res){
683            var hr;
684            if ( 'surveyRunId' in req.query ) {
685                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
686            } else {
687                hr = getDocumentsOfType('Response');
688            }
689            hr.handle(res.send.bind(res));
690        });
691    app.get('/api/responses/:id',
692        ensureAuthenticated,
693        ensureMIME(JSON_MIME),
694        makeDocGet_id('Response'));
695    app.post('/api/responses',
696        ensureAuthenticated,
697        ensureMIME(JSON_MIME),
698        function (req,res) {
699            var doc = req.body;
700            randomToken()
701            .handle({
702                201: function(token) {
703                    doc.secret = token;
704                    return postDocument('Response',doc);
705                }
706            })
707            .handle(res.send.bind(res));
708        });
709    app.put('/api/responses/:id',
710        ensureAuthenticated,
711        ensureMIME(JSON_MIME),
712        function (req,res) {
713            var id = req.params.id;
714            var doc = req.body;
715            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
716            var hr;
717            if ( typeof doc.secret === 'undefined' ) {
718                hr = randomToken()
719                .handle({
720                    201: function(token) {
721                        doc.secret = token;
722                        return putDocument(id,rev,'Response',doc);
723                    }
724                });
725            } else {
726                hr = putDocument(id,rev,'Response',doc);
727            }
728            hr.handle(res.send.bind(res));
729        });
730    app.delete('/api/responses/:id',
731        ensureAuthenticated,
732        ensureMIME(JSON_MIME),
733        makeDocDel_id('Response'));
734
735    //respondents api
736    function isSurveyRunRunning(surveyRun) {
737        var now = new Date();
738        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
739        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
740        return afterStart && beforeEnd;
741    }
742    app.get('/api/open/responses/:id',
743        ensureMIME(JSON_MIME),
744        function(req,res) {
745            var id = req.params.id;
746            var rev = etags.parse(req.header('If-Non-Match'))[0];
747            var secret = req.query.secret;
748            getDocument(id,rev,'Response')
749            .handle({
750                200: function(response) {
751                    if ( response.secret === secret ) {
752                        return getDocument(response.surveyRunId,[],'SurveyRun')
753                        .handle({
754                            200: function(surveyRun) {
755                                if ( !isSurveyRunRunning(surveyRun) ) {
756                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
757                                } else {
758                                    response._surveyRun = surveyRun;
759                                    return response;
760                                }
761                            }
762                        });
763                    } else {
764                        return new HTTPResult(403,{error: "Wrong secret for response"});
765                    }
766                }
767            })
768            .handle(res.send.bind(res));
769        });
770    app.post('/api/open/responses',
771        ensureMIME(JSON_MIME),
772        function(req,res) {
773            var secret = req.query.secret;
774            var response = req.body;
775            delete response._surveyRun;
776            getDocument(response.surveyRunId,[],'SurveyRun')
777            .handle({
778                200: function(surveyRun) {
779                    if ( surveyRun.secret !== secret ) {
780                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
781                    } else if ( !isSurveyRunRunning(surveyRun) ) {
782                        return new HTTPResult(404,{error:"Survey is not running anymore."});
783                    } else if ( surveyRun.mode === 'closed' ) {
784                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
785                    } else {
786                        return randomToken()
787                        .handle({
788                            201: function(token) {
789                                response.secret = token;
790                                return postDocument('Response',response)
791                                .handle({
792                                    201: function(doc){
793                                        doc._surveyRun = surveyRun;
794                                        return doc;
795                                    }
796                                });
797                            }
798                        });
799                    }
800                }
801            })
802            .handle(res.send.bind(res));
803        });
804    app.put('/api/open/responses/:id',
805        ensureMIME(JSON_MIME),
806        function(req,res){
807            var id = req.params.id;
808            var doc = req.body;
809            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
810            var secret = req.query.secret || doc.secret;
811            delete doc._surveyRun;
812            getDocument(id,[],'Response')
813            .handle({
814                200: function(prev) {
815                    if ( prev.secret !== secret ) {
816                        return new HTTPResult(403,{error: "Secrets are not the same."});
817                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
818                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
819                    } else {
820                        doc.secret = secret; // restore in case it got lost or was changed
821                        return getDocument(doc.surveyRunId,[],'SurveyRun')
822                        .handle({
823                            200: function(surveyRun) {
824                                if ( !isSurveyRunRunning(surveyRun) ) {
825                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
826                                } else {
827                                    return putDocument(id,rev,'Response',doc)
828                                    .handle({
829                                        200: function(doc) {
830                                            doc._surveyRun = surveyRun;
831                                            return new HTTPResult(201,doc);
832                                        }
833                                    });
834                                }
835                            }
836                        });
837                    }
838                }
839            })
840            .handle(res.send.bind(res));
841        });
842    app.delete('/api/open/responses/:id',
843        ensureMIME(JSON_MIME),
844        function(req,res){
845            var id = req.params.id;
846            var doc = req.body;
847            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
848            var secret = req.query.secret || doc.secret;
849            delete doc._surveyRun;
850            getDocument(id,[],'Response')
851            .handle({
852                200: function(prev) {
853                    if ( prev.secret !== secret ) {
854                        return new HTTPResult(403,{error: "Secrets are not the same."});
855                    } else {
856                        return getDocument(doc.surveyRunId,[],'SurveyRun')
857                        .handle({
858                            200: function(surveyRun) {
859                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
860                                    return deleteDocument(id,rev,doc);
861                                } else {
862                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
863                                }
864                            }
865                        });
866                    }
867                }
868            })
869            .handle(res.send.bind(res));
870        });
871
872    // uuids
873    app.get('/api/uuids',
874        ensureAuthenticated,
875        ensureMIME(JSON_MIME),
876        function(req,res){
877            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
878            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
879                                           handleUnknownError)
880            .handle({
881                200: function(res) {
882                    return res.uuids;
883                },
884                default: handleUnknownResponse
885            })
886            .handle(res.send.bind(res));
887        });
888
889    return app;
890}
891
892function responsesToVariables(responses) {
893    return _.map(responses, responseToVariables);
894}
895
896function responseToVariables(response) {
897    var result = flattenObject(response.answers);
898    return result;
899}
900
901function flattenObject(value) {
902    var result = {};
903    (function agg(val,key,res){
904        if ( _.isObject(val) ) {
905            var keys = _.keys(val);
906            // FIXME : dirty hack for questions with only one input
907            if ( keys.length === 1 ) {
908                agg(val[keys[0]],key,res);
909            } else {
910                _.forEach(val, function(v,k){
911                    agg(v,(key ? key+'.' : '')+k,res);
912                });
913            }
914        } else if ( _.isArray(val) ) {
915            // FIXME : dirty hack for questions with only one input
916            if ( val.length === 1 ) {
917                agg(val[0],key,res);
918            } else {
919                _.forEach(val, function(v,i){
920                    agg(v,(key ? key+'.' : '')+i,res);
921                });
922            }
923        } else {
924            res[key] = val;
925        }
926    })(value,null,result);
927    return result;
928}
929
930function writeObjectsToCSVStream(objects, stream, prelude) {
931    var keys = _.chain(objects)
932                .map(_.keys)
933                .flatten()
934                .uniq()
935                .value();
936    var idxs = {};
937    _.forEach(keys, function(key,idx){
938        idxs[key] = idx;
939    });
940    var writer = new CSV.CsvWriter(stream);
941    if ( prelude ) {
942        _.forEach(prelude, function(val,key){
943            writer.writeRecord([key,val]);
944        });
945    }
946    writer.writeRecord(keys);
947    _.forEach(objects, function(obj){
948        var row = [];
949        _.forEach(obj, function(val,key){
950            row[idxs[key]] = val;
951        });
952        writer.writeRecord(row);
953    });
954}
955
956function randomToken() {
957    var result = new HTTPResult();
958    cryptoken(8)
959    .then(function(token){
960        result.set(201,token);
961    }, function(ex){
962        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
963    });
964    return result;
965}
Note: See TracBrowser for help on using the repository browser.