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

Last change on this file since 496 was 496, checked in by hendrikvanantwerpen, 11 years ago

Reject surveys with duplicate questions.

File size: 33.5 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    function areSurveyQuestionsUnique(survey) {
505        return _.chain(survey.questions)
506                .map(function(q){ return q.code; })
507                .uniq()
508                .value().length === survey.questions.length;
509    }
510    app.get('/api/surveys',
511        ensureAuthenticated,
512        ensureMIME(JSON_MIME),
513        function(req,res) {
514            var url;
515            if ( 'drafts' in req.query ) {
516                url = '_design/surveys/_view/drafts';
517            } else if ( 'published' in req.query ) {
518                url = '_design/surveys/_view/published';
519            } else {
520                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
521            }
522            HTTPResult.fromResponsePromise(couch.get(url).response,
523                                           handleUnknownError)
524            .handle({
525                200: function(result) {
526                    return _.map(result.rows, function(item) { return item.value; });
527                },
528                default: handleUnknownResponse
529            })
530            .handle(res.send.bind(res));
531        });
532    app.post('/api/surveys',
533        ensureAuthenticated,
534        ensureMIME(JSON_MIME),
535        function(req,res) {
536            var doc = req.body;
537            var hr;
538            if ( !areSurveyQuestionsUnique(doc) ) {
539                hr = new HTTPResult(400,{error:"Survey contains duplicate questions."});
540            } else {
541                hr = postDocument('Survey',doc);
542            }
543            hr.handle(res.send.bind(res));
544        });
545    app.get('/api/surveys/:id',
546        ensureAuthenticated,
547        ensureMIME(JSON_MIME),
548        makeDocGet_id('Survey'));
549    app.put('/api/surveys/:id',
550        ensureAuthenticated,
551        ensureMIME(JSON_MIME),
552        function(req,res) {
553            var id = req.params.id;
554            var doc = req.body;
555            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
556            var hr;
557            if ( !areSurveyQuestionsUnique(doc) ) {
558                new HTTPResult(400,{error:"Survey contains duplicate questions."})
559                .handle(res.send.bind(res));
560            } else {
561                putDocument(id,rev,'Survey',doc)
562                .handle({
563                    201: function(doc) {
564                        res.set({
565                            'ETag': etags.format([doc._rev])
566                        }).send(201, doc);
567                    },
568                    default: res.send.bind(res)
569                });
570            }
571        });
572    app.delete('/api/surveys/:id',
573        ensureAuthenticated,
574        ensureMIME(JSON_MIME),
575        makeDocDel_id('Survey'));
576
577    // SurveyRuns
578    app.get('/api/surveyRuns',
579        ensureAuthenticated,
580        ensureMIME(JSON_MIME),
581        makeDocsGet('SurveyRun'));
582    app.post('/api/surveyRuns',
583        ensureAuthenticated,
584        ensureMIME(JSON_MIME),
585        function(req,res) {
586            var doc = req.body;
587            randomToken()
588            .handle({
589                201: function(token) {
590                    doc.secret = token;
591                    return postDocument('SurveyRun',doc);
592                }
593            })
594            .handle(res.send.bind(res));
595        });
596    app.get('/api/surveyRuns/:id',
597        ensureAuthenticated,
598        ensureMIME(JSON_MIME),
599        makeDocGet_id('SurveyRun'));
600    app.put('/api/surveyRuns/:id',
601        ensureAuthenticated,
602        ensureMIME(JSON_MIME),
603        function (req,res) {
604            var id = req.params.id;
605            var doc = req.body;
606            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
607            var hr;
608            if ( typeof doc.secret === 'undefined' ) {
609                hr = randomToken()
610                .handle({
611                    201: function(token) {
612                        doc.secret = token;
613                        return putDocument(id,rev,'SurveyRun',doc);
614                    }
615                });
616            } else {
617                hr = putDocument(id,rev,'SurveyRun',doc);
618            }
619            hr.handle(res.send.bind(res));
620        });
621    app.delete('/api/surveyRuns/:id',
622        ensureAuthenticated,
623        ensureMIME(JSON_MIME),
624        makeDocDel_id('SurveyRun'));
625    app.get('/api/surveyRuns/:id/responses',
626        ensureAuthenticated,
627        ensureMIME(JSON_MIME),
628        function(req,res) {
629            var id = req.params.id;
630            getResponsesBySurveyRunId(id)
631            .handle(res.send.bind(res));
632        });
633    app.get('/api/surveyRuns/:id/responses.csv',
634        ensureAuthenticated,
635        ensureMIME(CSV_MIME),
636        function(req, res) {
637            var id = req.params.id;
638            getResponsesBySurveyRunId(id)
639            .handle({
640                200: function(responses) {
641                    var flatResponses = responsesToVariables(responses);
642                    res.set({
643                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
644                    });
645                    res.status(200);
646                    writeObjectsToCSVStream(flatResponses, res);
647                    res.end();
648                },
649                default: res.send.bind(res)
650            });
651        });
652   
653    // Responses
654    function getResponsesBySurveyRunId(surveyRunId) {
655        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
656        return HTTPResult.fromResponsePromise(couch.get(url).response,
657                                              handleUnknownError)
658        .handle({
659            200: handleRowValues,
660            default: handleUnknownResponse
661        });
662    }
663    app.get('/api/responses',
664        ensureAuthenticated,
665        ensureMIME(JSON_MIME),
666        function(req,res){
667            var hr;
668            if ( 'surveyRunId' in req.query ) {
669                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
670            } else {
671                hr = getDocumentsOfType('Response');
672            }
673            hr.handle(res.send.bind(res));
674        });
675    app.get('/api/responses/:id',
676        ensureAuthenticated,
677        ensureMIME(JSON_MIME),
678        makeDocGet_id('Response'));
679    app.post('/api/responses',
680        ensureAuthenticated,
681        ensureMIME(JSON_MIME),
682        function (req,res) {
683            var doc = req.body;
684            randomToken()
685            .handle({
686                201: function(token) {
687                    doc.secret = token;
688                    return postDocument('Response',doc);
689                }
690            })
691            .handle(res.send.bind(res));
692        });
693    app.put('/api/responses/:id',
694        ensureAuthenticated,
695        ensureMIME(JSON_MIME),
696        function (req,res) {
697            var id = req.params.id;
698            var doc = req.body;
699            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
700            var hr;
701            if ( typeof doc.secret === 'undefined' ) {
702                hr = randomToken()
703                .handle({
704                    201: function(token) {
705                        doc.secret = token;
706                        return putDocument(id,rev,'Response',doc);
707                    }
708                });
709            } else {
710                hr = putDocument(id,rev,'Response',doc);
711            }
712            hr.handle(res.send.bind(res));
713        });
714    app.delete('/api/responses/:id',
715        ensureAuthenticated,
716        ensureMIME(JSON_MIME),
717        makeDocDel_id('Response'));
718
719    //respondents api
720    function isSurveyRunRunning(surveyRun) {
721        var now = new Date();
722        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
723        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
724        return afterStart && beforeEnd;
725    }
726    app.get('/api/open/responses/:id',
727        ensureMIME(JSON_MIME),
728        function(req,res) {
729            var id = req.params.id;
730            var rev = etags.parse(req.header('If-Non-Match'))[0];
731            var secret = req.query.secret;
732            getDocument(id,rev,'Response')
733            .handle({
734                200: function(response) {
735                    if ( response.secret === secret ) {
736                        return getDocument(response.surveyRunId,[],'SurveyRun')
737                        .handle({
738                            200: function(surveyRun) {
739                                if ( !isSurveyRunRunning(surveyRun) ) {
740                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
741                                } else {
742                                    response._surveyRun = surveyRun;
743                                    return response;
744                                }
745                            }
746                        });
747                    } else {
748                        return new HTTPResult(403,{error: "Wrong secret for response"});
749                    }
750                }
751            })
752            .handle(res.send.bind(res));
753        });
754    app.post('/api/open/responses',
755        ensureMIME(JSON_MIME),
756        function(req,res) {
757            var secret = req.query.secret;
758            var response = req.body;
759            delete response._surveyRun;
760            getDocument(response.surveyRunId,[],'SurveyRun')
761            .handle({
762                200: function(surveyRun) {
763                    if ( surveyRun.secret !== secret ) {
764                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
765                    } else if ( !isSurveyRunRunning(surveyRun) ) {
766                        return new HTTPResult(404,{error:"Survey is not running anymore."});
767                    } else if ( surveyRun.mode === 'closed' ) {
768                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
769                    } else {
770                        return randomToken()
771                        .handle({
772                            201: function(token) {
773                                response.secret = token;
774                                return postDocument('Response',response)
775                                .handle({
776                                    201: function(doc){
777                                        doc._surveyRun = surveyRun;
778                                        return doc;
779                                    }
780                                });
781                            }
782                        });
783                    }
784                }
785            })
786            .handle(res.send.bind(res));
787        });
788    app.put('/api/open/responses/:id',
789        ensureMIME(JSON_MIME),
790        function(req,res){
791            var id = req.params.id;
792            var doc = req.body;
793            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
794            var secret = req.query.secret || doc.secret;
795            delete doc._surveyRun;
796            getDocument(id,[],'Response')
797            .handle({
798                200: function(prev) {
799                    if ( prev.secret !== secret ) {
800                        return new HTTPResult(403,{error: "Secrets are not the same."});
801                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
802                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
803                    } else {
804                        doc.secret = secret; // restore in case it got lost or was changed
805                        return getDocument(doc.surveyRunId,[],'SurveyRun')
806                        .handle({
807                            200: function(surveyRun) {
808                                if ( !isSurveyRunRunning(surveyRun) ) {
809                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
810                                } else {
811                                    return putDocument(id,rev,'Response',doc)
812                                    .handle({
813                                        200: function(doc) {
814                                            doc._surveyRun = surveyRun;
815                                            return new HTTPResult(201,doc);
816                                        }
817                                    });
818                                }
819                            }
820                        });
821                    }
822                }
823            })
824            .handle(res.send.bind(res));
825        });
826    app.delete('/api/open/responses/:id',
827        ensureMIME(JSON_MIME),
828        function(req,res){
829            var id = req.params.id;
830            var doc = req.body;
831            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
832            var secret = req.query.secret || doc.secret;
833            delete doc._surveyRun;
834            getDocument(id,[],'Response')
835            .handle({
836                200: function(prev) {
837                    if ( prev.secret !== secret ) {
838                        return new HTTPResult(403,{error: "Secrets are not the same."});
839                    } else {
840                        return getDocument(doc.surveyRunId,[],'SurveyRun')
841                        .handle({
842                            200: function(surveyRun) {
843                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
844                                    return deleteDocument(id,rev,doc);
845                                } else {
846                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
847                                }
848                            }
849                        });
850                    }
851                }
852            })
853            .handle(res.send.bind(res));
854        });
855
856    // uuids
857    app.get('/api/uuids',
858        ensureAuthenticated,
859        ensureMIME(JSON_MIME),
860        function(req,res){
861            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
862            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
863                                           handleUnknownError)
864            .handle({
865                200: function(res) {
866                    return res.uuids;
867                },
868                default: handleUnknownResponse
869            })
870            .handle(res.send.bind(res));
871        });
872
873    return app;
874}
875
876function responsesToVariables(responses) {
877    return _.map(responses, responseToVariables);
878}
879
880function responseToVariables(response) {
881    var result = flattenObject(response.answers);
882    return result;
883}
884
885function flattenObject(value) {
886    var result = {};
887    (function agg(val,key,res){
888        if ( _.isObject(val) ) {
889            var keys = _.keys(val);
890            // FIXME : dirty hack for questions with only one input
891            if ( keys.length === 1 ) {
892                agg(val[keys[0]],key,res);
893            } else {
894                _.forEach(val, function(v,k){
895                    agg(v,(key ? key+'.' : '')+k,res);
896                });
897            }
898        } else if ( _.isArray(val) ) {
899            // FIXME : dirty hack for questions with only one input
900            if ( val.length === 1 ) {
901                agg(val[0],key,res);
902            } else {
903                _.forEach(val, function(v,i){
904                    agg(v,(key ? key+'.' : '')+i,res);
905                });
906            }
907        } else {
908            res[key] = val;
909        }
910    })(value,null,result);
911    return result;
912}
913
914function writeObjectsToCSVStream(objects, stream, prelude) {
915    var keys = _.chain(objects)
916                .map(_.keys)
917                .flatten()
918                .uniq()
919                .value();
920    var idxs = {};
921    _.forEach(keys, function(key,idx){
922        idxs[key] = idx;
923    });
924    var writer = new CSV.CsvWriter(stream);
925    if ( prelude ) {
926        _.forEach(prelude, function(val,key){
927            writer.writeRecord([key,val]);
928        });
929    }
930    writer.writeRecord(keys);
931    _.forEach(objects, function(obj){
932        var row = [];
933        _.forEach(obj, function(val,key){
934            row[idxs[key]] = val;
935        });
936        writer.writeRecord(row);
937    });
938}
939
940function randomToken() {
941    var result = new HTTPResult();
942    cryptoken(8)
943    .then(function(token){
944        result.set(201,token);
945    }, function(ex){
946        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
947    });
948    return result;
949}
Note: See TracBrowser for help on using the repository browser.