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

Last change on this file since 495 was 495, checked in by hendrikvanantwerpen, 11 years ago
File size: 32.2 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        if ( doc.type === type && validator(doc, schema).valid ) {
228            var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
229            return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response,
230                                                  handleUnknownError)
231            .handle({
232                201: function(res){
233                    doc._id = res.id;
234                    doc._rev = res.rev;
235                    return doc;
236                },
237                409: function(error) {
238                    return {error: error.reason};
239                },
240                default: handleUnknownResponse
241            });
242        } else {
243            return new HTTPResult(400,{error: "Document failed schema verification."});
244        }
245    }
246    function deleteDocument(id,rev) {
247        if ( rev ) {
248            var opts = {headers:{'If-Match':'"'+rev+'"'}};
249            return HTTPResult.fromResponsePromise(couch.delete(id,opts).response,
250                                                  handleUnknownError)
251            .handle({
252                200: identity,
253                409: function(error) {
254                    return {error: error.reason};
255                },
256                default: handleUnknownResponse
257            });
258        } else {
259            return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
260        }
261    }
262    function postDocument(type,doc) {
263        var priv = stripAndReturnPrivates(doc);
264        if ( doc.type === type && validator(doc, schema).valid ) {
265            return HTTPResult.fromResponsePromise(couch.post(doc).response,
266                                                  handleUnknownError)
267            .handle({
268                201: function(response) {
269                    doc._id = response.id;
270                    doc._rev = response.rev;
271                    return doc;
272                },
273                default: handleUnknownResponse
274            });
275        } else {
276            return new HTTPResult(400,{error: "Document failed schema verification."});
277        }
278    }
279
280    function makeDocsGet(type) {
281        return function(req,res) {
282            getDocumentsOfType(type)
283            .handle(res.send.bind(res));
284        };
285    }
286    function makeDocGet_id(type) {
287        return function(req,res) {
288            var id = req.params.id;
289            var rev = etags.parse(req.header('If-Non-Match'))[0];
290            getDocument(id,rev,type)
291            .handle({
292                200: function(doc){
293                    res.set({
294                        'ETag': etags.format([doc._rev])
295                    }).send(200, doc);
296                },
297                default: res.send.bind(res)
298            });
299        };
300    }
301    function makeDocPut_id(type) {
302        return function(req,res) {
303            var id = req.params.id;
304            var doc = req.body;
305            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
306            putDocument(id,rev,type,doc)
307            .handle({
308                201: function(doc) {
309                    res.set({
310                        'ETag': etags.format([doc._rev])
311                    }).send(201, doc);
312                },
313                default: res.send.bind(res)
314            });
315        };
316    }
317    function makeDocDel_id(type) {
318        return function(req,res) {
319            var id = req.params.id;
320            var doc = req.body;
321            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
322            deleteDocument(id,rev)
323            .handle(res.send.bind(res));
324        };
325    }
326    function makeDocPost(type) {
327        return function(req,res) {
328            var doc = req.body;
329            postDocument(type,doc)
330            .handle({
331                201: function(doc) {
332                    res.set({
333                        'ETag': etags.format([doc._rev])
334                    }).send(201, doc);
335                },
336                default: res.send.bind(res)
337            });
338        };
339    }
340
341    // Questions
342    function getQuestionsWithCode(code) {
343        var url = '_design/questions/_view/by_code';
344        var query = {include_docs:true,key:code};
345        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
346                                              handleUnknownError)
347        .handle({
348            200: handleRowDocs,
349            default: handleUnknownResponse
350        });
351    }
352    function getQuestionsAndCodes() {
353        var url = '_design/questions/_view/by_code';
354        var query = {include_docs:true};
355        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
356                                              handleUnknownError)
357        .handle({
358            200: handleRowDictOfDocs,
359            default: handleUnknownResponse
360        });
361    }
362    function getQuestionsWithTopic(topic) {
363        var url = '_design/questions/_view/all_topics';
364        var query = {reduce:false,include_docs:true,key:topic};
365        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
366                                              handleUnknownError)
367        .handle({
368            200: handleRowDocs,
369            default: handleUnknownResponse
370        });
371    }
372    function getQuestionsWithCategoryAndTopic(category,topic) {
373        var hasTopic = typeof topic !== 'undefined';
374        var url = '_design/questions/_view/all';
375        var query = {reduce:false,include_docs:true,
376                     startkey:hasTopic?[category,topic]:[category],
377                     endkey:hasTopic?[category,topic]:[category,{}]};
378        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
379                                              handleUnknownError)
380        .handle({
381            200: handleRowDocs,
382            default: handleUnknownResponse
383        });
384    }
385    app.get('/api/questions',
386        ensureAuthenticated,
387        ensureMIME(JSON_MIME),
388        function(req,res) {
389            var hr;
390            if ( 'category' in req.query ) {
391                hr = getQuestionsWithCategoryAndTopic(req.query.category,req.query.topic);
392            } else if ( 'topic' in req.query ) {
393                hr = getQuestionsWithTopic(req.query.topic);
394            } else if ( 'code' in req.query ) {
395                hr = getQuestionsWithCode(req.query.code);
396            }
397            hr.handle(res.send.bind(res));
398        });
399    app.post('/api/questions',
400        ensureAuthenticated,
401        ensureMIME(JSON_MIME),
402        function (req,res) {
403            var doc = req.body;
404            getQuestionsWithCode(doc.code)
405            .handle({
406                200: function(others) {
407                    if ( others.length > 0 ) {
408                        return new HTTPResult(403,{error:"Other question with this code already exists."});
409                    } else {
410                        return postDocument('Question',doc);
411                    }
412                }
413            })
414            .handle(res.send.bind(res));
415        });
416    app.get('/api/questions/:id',
417        ensureAuthenticated,
418        ensureMIME(JSON_MIME),
419        makeDocGet_id('Question'));
420    app.put('/api/questions/:id',
421        ensureAuthenticated,
422        ensureMIME(JSON_MIME),
423        function (req,res) {
424            var id = req.params.id;
425            var doc = req.body;
426            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
427            getQuestionsWithCode(doc.code)
428            .handle({
429                200: function(others) {
430                    if ( others.length > 0 && _.some(others,function(other){ return other._id !== id; })  ) {
431                        return new HTTPResult(403,{error:"Other question with this code already exists."});
432                    } else {
433                        return putDocument(id,rev,'Question',doc);
434                    }
435                }
436            })
437            .handle(res.send.bind(res));
438        });
439    app.delete('/api/questions/:id',
440        ensureAuthenticated,
441        ensureMIME(JSON_MIME),
442        makeDocDel_id('Question'));
443
444
445    // Categories and topics
446    function getTopicsWithCategory(category) {
447        var url = '_design/questions/_view/all';
448        return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
449                                       handleUnknownError)
450        .handle({
451            200: function(result) {
452                return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
453            },
454            default: handleUnknownResponse
455        });
456    }
457    app.get('/api/categories',
458        ensureAuthenticated,
459        ensureMIME(JSON_MIME),
460        function(req,res) {
461            var url = '_design/questions/_view/all';
462            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
463                                           handleUnknownError)
464            .handle({
465                200: function(result) {
466                    return _.map(result.rows, function(item) {
467                        return { name:item.key[0], count:item.value };
468                    });
469                },
470                default: handleUnknownResponse
471            })
472            .handle(res.send.bind(res));
473        });
474    app.get('/api/categories/:category/topics',
475        ensureAuthenticated,
476        ensureMIME(JSON_MIME),
477        function(req,res) {
478            var category = req.params.category;
479            getTopicsWithCategory(category)
480            .handle(res.send.bind(res));
481        });
482    app.get('/api/topics',
483        ensureAuthenticated,
484        ensureMIME(JSON_MIME),
485        function(req,res) {
486            var url = '_design/questions/_view/all_topics';
487            var hr;
488            if ( 'category' in req.query ) {
489                hr = getTopicsWithCategory(req.query.category);
490            } else {
491                hr = HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
492                                                    handleUnknownError)
493                .handle({
494                    200: function(result) {
495                        return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
496                    },
497                    default: handleUnknownResponse
498                });
499            }
500            hr.handle(res.send.bind(res));
501        });
502
503    // Surveys
504    app.get('/api/surveys',
505        ensureAuthenticated,
506        ensureMIME(JSON_MIME),
507        function(req,res) {
508            var url;
509            if ( 'drafts' in req.query ) {
510                url = '_design/surveys/_view/drafts';
511            } else if ( 'published' in req.query ) {
512                url = '_design/surveys/_view/published';
513            } else {
514                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
515            }
516            HTTPResult.fromResponsePromise(couch.get(url).response,
517                                           handleUnknownError)
518            .handle({
519                200: function(result) {
520                    return _.map(result.rows, function(item) { return item.value; });
521                },
522                default: handleUnknownResponse
523            })
524            .handle(res.send.bind(res));
525        });
526    app.post('/api/surveys',
527        ensureAuthenticated,
528        ensureMIME(JSON_MIME),
529        makeDocPost('Survey'));
530    app.get('/api/surveys/:id',
531        ensureAuthenticated,
532        ensureMIME(JSON_MIME),
533        makeDocGet_id('Survey'));
534    app.put('/api/surveys/:id',
535        ensureAuthenticated,
536        ensureMIME(JSON_MIME),
537        makeDocPut_id('Survey'));
538    app.delete('/api/surveys/:id',
539        ensureAuthenticated,
540        ensureMIME(JSON_MIME),
541        makeDocDel_id('Survey'));
542
543    // SurveyRuns
544    app.get('/api/surveyRuns',
545        ensureAuthenticated,
546        ensureMIME(JSON_MIME),
547        makeDocsGet('SurveyRun'));
548    app.post('/api/surveyRuns',
549        ensureAuthenticated,
550        ensureMIME(JSON_MIME),
551        function(req,res) {
552            var doc = req.body;
553            randomToken()
554            .handle({
555                201: function(token) {
556                    doc.secret = token;
557                    return postDocument('SurveyRun',doc);
558                }
559            })
560            .handle(res.send.bind(res));
561        });
562    app.get('/api/surveyRuns/:id',
563        ensureAuthenticated,
564        ensureMIME(JSON_MIME),
565        makeDocGet_id('SurveyRun'));
566    app.put('/api/surveyRuns/:id',
567        ensureAuthenticated,
568        ensureMIME(JSON_MIME),
569        function (req,res) {
570            var id = req.params.id;
571            var doc = req.body;
572            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
573            var hr;
574            if ( typeof doc.secret === 'undefined' ) {
575                hr = randomToken()
576                .handle({
577                    201: function(token) {
578                        doc.secret = token;
579                        return putDocument(id,rev,'SurveyRun',doc);
580                    }
581                });
582            } else {
583                hr = putDocument(id,rev,'SurveyRun',doc);
584            }
585            hr.handle(res.send.bind(res));
586        });
587    app.delete('/api/surveyRuns/:id',
588        ensureAuthenticated,
589        ensureMIME(JSON_MIME),
590        makeDocDel_id('SurveyRun'));
591    app.get('/api/surveyRuns/:id/responses',
592        ensureAuthenticated,
593        ensureMIME(JSON_MIME),
594        function(req,res) {
595            var id = req.params.id;
596            getResponsesBySurveyRunId(id)
597            .handle(res.send.bind(res));
598        });
599    app.get('/api/surveyRuns/:id/responses.csv',
600        ensureAuthenticated,
601        ensureMIME(CSV_MIME),
602        function(req, res) {
603            var id = req.params.id;
604            getResponsesBySurveyRunId(id)
605            .handle({
606                200: function(responses) {
607                    var flatResponses = responsesToVariables(responses);
608                    res.set({
609                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
610                    });
611                    res.status(200);
612                    writeObjectsToCSVStream(flatResponses, res);
613                    res.end();
614                },
615                default: res.send.bind(res)
616            });
617        });
618   
619    // Responses
620    function getResponsesBySurveyRunId(surveyRunId) {
621        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
622        return HTTPResult.fromResponsePromise(couch.get(url).response,
623                                              handleUnknownError)
624        .handle({
625            200: handleRowValues,
626            default: handleUnknownResponse
627        });
628    }
629    app.get('/api/responses',
630        ensureAuthenticated,
631        ensureMIME(JSON_MIME),
632        function(req,res){
633            var hr;
634            if ( 'surveyRunId' in req.query ) {
635                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
636            } else {
637                hr = getDocumentsOfType('Response');
638            }
639            hr.handle(res.send.bind(res));
640        });
641    app.get('/api/responses/:id',
642        ensureAuthenticated,
643        ensureMIME(JSON_MIME),
644        makeDocGet_id('Response'));
645    app.post('/api/responses',
646        ensureAuthenticated,
647        ensureMIME(JSON_MIME),
648        function (req,res) {
649            var doc = req.body;
650            randomToken()
651            .handle({
652                201: function(token) {
653                    doc.secret = token;
654                    return postDocument('Response',doc);
655                }
656            })
657            .handle(res.send.bind(res));
658        });
659    app.put('/api/responses/:id',
660        ensureAuthenticated,
661        ensureMIME(JSON_MIME),
662        function (req,res) {
663            var id = req.params.id;
664            var doc = req.body;
665            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
666            var hr;
667            if ( typeof doc.secret === 'undefined' ) {
668                hr = randomToken()
669                .handle({
670                    201: function(token) {
671                        doc.secret = token;
672                        return putDocument(id,rev,'Response',doc);
673                    }
674                });
675            } else {
676                hr = putDocument(id,rev,'Response',doc);
677            }
678            hr.handle(res.send.bind(res));
679        });
680    app.delete('/api/responses/:id',
681        ensureAuthenticated,
682        ensureMIME(JSON_MIME),
683        makeDocDel_id('Response'));
684
685    //respondents api
686    function isSurveyRunRunning(surveyRun) {
687        var now = new Date();
688        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
689        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
690        return afterStart && beforeEnd;
691    }
692    app.get('/api/open/responses/:id',
693        ensureMIME(JSON_MIME),
694        function(req,res) {
695            var id = req.params.id;
696            var rev = etags.parse(req.header('If-Non-Match'))[0];
697            var secret = req.query.secret;
698            getDocument(id,rev,'Response')
699            .handle({
700                200: function(response) {
701                    if ( response.secret === secret ) {
702                        return getDocument(response.surveyRunId,[],'SurveyRun')
703                        .handle({
704                            200: function(surveyRun) {
705                                if ( !isSurveyRunRunning(surveyRun) ) {
706                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
707                                } else {
708                                    response._surveyRun = surveyRun;
709                                    return response;
710                                }
711                            }
712                        });
713                    } else {
714                        return new HTTPResult(403,{error: "Wrong secret for response"});
715                    }
716                }
717            })
718            .handle(res.send.bind(res));
719        });
720    app.post('/api/open/responses',
721        ensureMIME(JSON_MIME),
722        function(req,res) {
723            var secret = req.query.secret;
724            var response = req.body;
725            delete response._surveyRun;
726            getDocument(response.surveyRunId,[],'SurveyRun')
727            .handle({
728                200: function(surveyRun) {
729                    if ( surveyRun.secret !== secret ) {
730                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
731                    } else if ( !isSurveyRunRunning(surveyRun) ) {
732                        return new HTTPResult(404,{error:"Survey is not running anymore."});
733                    } else if ( surveyRun.mode === 'closed' ) {
734                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
735                    } else {
736                        return randomToken()
737                        .handle({
738                            201: function(token) {
739                                response.secret = token;
740                                return postDocument('Response',response)
741                                .handle({
742                                    201: function(doc){
743                                        doc._surveyRun = surveyRun;
744                                        return doc;
745                                    }
746                                });
747                            }
748                        });
749                    }
750                }
751            })
752            .handle(res.send.bind(res));
753        });
754    app.put('/api/open/responses/:id',
755        ensureMIME(JSON_MIME),
756        function(req,res){
757            var id = req.params.id;
758            var doc = req.body;
759            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
760            var secret = req.query.secret || doc.secret;
761            delete doc._surveyRun;
762            getDocument(id,[],'Response')
763            .handle({
764                200: function(prev) {
765                    if ( prev.secret !== secret ) {
766                        return new HTTPResult(403,{error: "Secrets are not the same."});
767                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
768                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
769                    } else {
770                        doc.secret = secret; // restore in case it got lost or was changed
771                        return getDocument(doc.surveyRunId,[],'SurveyRun')
772                        .handle({
773                            200: function(surveyRun) {
774                                if ( !isSurveyRunRunning(surveyRun) ) {
775                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
776                                } else {
777                                    return putDocument(id,rev,'Response',doc)
778                                    .handle({
779                                        200: function(doc) {
780                                            doc._surveyRun = surveyRun;
781                                            return new HTTPResult(201,doc);
782                                        }
783                                    });
784                                }
785                            }
786                        });
787                    }
788                }
789            })
790            .handle(res.send.bind(res));
791        });
792    app.delete('/api/open/responses/:id',
793        ensureMIME(JSON_MIME),
794        function(req,res){
795            var id = req.params.id;
796            var doc = req.body;
797            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
798            var secret = req.query.secret || doc.secret;
799            delete doc._surveyRun;
800            getDocument(id,[],'Response')
801            .handle({
802                200: function(prev) {
803                    if ( prev.secret !== secret ) {
804                        return new HTTPResult(403,{error: "Secrets are not the same."});
805                    } else {
806                        return getDocument(doc.surveyRunId,[],'SurveyRun')
807                        .handle({
808                            200: function(surveyRun) {
809                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
810                                    return deleteDocument(id,rev,doc);
811                                } else {
812                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
813                                }
814                            }
815                        });
816                    }
817                }
818            })
819            .handle(res.send.bind(res));
820        });
821
822    // uuids
823    app.get('/api/uuids',
824        ensureAuthenticated,
825        ensureMIME(JSON_MIME),
826        function(req,res){
827            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
828            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
829                                           handleUnknownError)
830            .handle({
831                200: function(res) {
832                    return res.uuids;
833                },
834                default: handleUnknownResponse
835            })
836            .handle(res.send.bind(res));
837        });
838
839    return app;
840}
841
842function responsesToVariables(responses) {
843    return _.map(responses, responseToVariables);
844}
845
846function responseToVariables(response) {
847    var result = flattenObject(response.answers);
848    return result;
849}
850
851function flattenObject(value) {
852    var result = {};
853    (function agg(val,key,res){
854        if ( _.isObject(val) ) {
855            var keys = _.keys(val);
856            // FIXME : dirty hack for questions with only one input
857            if ( keys.length === 1 ) {
858                agg(val[keys[0]],key,res);
859            } else {
860                _.forEach(val, function(v,k){
861                    agg(v,(key ? key+'.' : '')+k,res);
862                });
863            }
864        } else if ( _.isArray(val) ) {
865            // FIXME : dirty hack for questions with only one input
866            if ( val.length === 1 ) {
867                agg(val[0],key,res);
868            } else {
869                _.forEach(val, function(v,i){
870                    agg(v,(key ? key+'.' : '')+i,res);
871                });
872            }
873        } else {
874            res[key] = val;
875        }
876    })(value,null,result);
877    return result;
878}
879
880function writeObjectsToCSVStream(objects, stream, prelude) {
881    var keys = _.chain(objects)
882                .map(_.keys)
883                .flatten()
884                .uniq()
885                .value();
886    var idxs = {};
887    _.forEach(keys, function(key,idx){
888        idxs[key] = idx;
889    });
890    var writer = new CSV.CsvWriter(stream);
891    if ( prelude ) {
892        _.forEach(prelude, function(val,key){
893            writer.writeRecord([key,val]);
894        });
895    }
896    writer.writeRecord(keys);
897    _.forEach(objects, function(obj){
898        var row = [];
899        _.forEach(obj, function(val,key){
900            row[idxs[key]] = val;
901        });
902        writer.writeRecord(row);
903    });
904}
905
906function randomToken() {
907    var result = new HTTPResult();
908    cryptoken(8)
909    .then(function(token){
910        result.set(201,token);
911    }, function(ex){
912        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
913    });
914    return result;
915}
Note: See TracBrowser for help on using the repository browser.