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

Last change on this file since 492 was 492, checked in by hendrikvanantwerpen, 11 years ago
  • Enable/disable buttons on content change.
  • One place to do date formatting, because it was going wrong again.
  • Serialize questions in survey properly.
  • _ComplexValueMixin consumes submit events, but does trigger outer forms if present.
  • Trigger dialog show/hide for login only after previous effect is finished.
  • Check that documents are actually valid, not just that validator returned a result.
  • Validate email and timestamp formats.
  • Prepared for live runs.
File size: 31.9 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    app.get('/api/categories',
447        ensureAuthenticated,
448        ensureMIME(JSON_MIME),
449        function(req,res) {
450            var url = '_design/questions/_view/all';
451            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
452                                           handleUnknownError)
453            .handle({
454                200: function(result) {
455                    return _.map(result.rows, function(item) {
456                        return { name:item.key[0], count:item.value };
457                    });
458                },
459                default: handleUnknownResponse
460            })
461            .handle(res.send.bind(res));
462        });
463    app.get('/api/categories/:category/topics',
464        ensureAuthenticated,
465        ensureMIME(JSON_MIME),
466        function(req,res) {
467            var category = req.params.category;
468            var url = '_design/questions/_view/all';
469            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
470                                           handleUnknownError)
471            .handle({
472                200: function(result) {
473                    return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
474                },
475                default: handleUnknownResponse
476            })
477            .handle(res.send.bind(res));
478        });
479    app.get('/api/topics',
480        ensureAuthenticated,
481        ensureMIME(JSON_MIME),
482        function(req,res) {
483            var url = '_design/questions/_view/all_topics';
484            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
485                                           handleUnknownError)
486            .handle({
487                200: function(result) {
488                    return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
489                },
490                default: handleUnknownResponse
491            })
492            .handle(res.send.bind(res));
493        });
494
495    // Surveys
496    app.get('/api/surveys',
497        ensureAuthenticated,
498        ensureMIME(JSON_MIME),
499        function(req,res) {
500            var url;
501            if ( 'drafts' in req.query ) {
502                url = '_design/surveys/_view/drafts';
503            } else if ( 'published' in req.query ) {
504                url = '_design/surveys/_view/published';
505            } else {
506                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
507            }
508            HTTPResult.fromResponsePromise(couch.get(url).response,
509                                           handleUnknownError)
510            .handle({
511                200: function(result) {
512                    return _.map(result.rows, function(item) { return item.value; });
513                },
514                default: handleUnknownResponse
515            })
516            .handle(res.send.bind(res));
517        });
518    app.post('/api/surveys',
519        ensureAuthenticated,
520        ensureMIME(JSON_MIME),
521        makeDocPost('Survey'));
522    app.get('/api/surveys/:id',
523        ensureAuthenticated,
524        ensureMIME(JSON_MIME),
525        makeDocGet_id('Survey'));
526    app.put('/api/surveys/:id',
527        ensureAuthenticated,
528        ensureMIME(JSON_MIME),
529        makeDocPut_id('Survey'));
530    app.delete('/api/surveys/:id',
531        ensureAuthenticated,
532        ensureMIME(JSON_MIME),
533        makeDocDel_id('Survey'));
534
535    // SurveyRuns
536    app.get('/api/surveyRuns',
537        ensureAuthenticated,
538        ensureMIME(JSON_MIME),
539        makeDocsGet('SurveyRun'));
540    app.post('/api/surveyRuns',
541        ensureAuthenticated,
542        ensureMIME(JSON_MIME),
543        function(req,res) {
544            var doc = req.body;
545            randomToken()
546            .handle({
547                201: function(token) {
548                    doc.secret = token;
549                    return postDocument('SurveyRun',doc);
550                }
551            })
552            .handle(res.send.bind(res));
553        });
554    app.get('/api/surveyRuns/:id',
555        ensureAuthenticated,
556        ensureMIME(JSON_MIME),
557        makeDocGet_id('SurveyRun'));
558    app.put('/api/surveyRuns/:id',
559        ensureAuthenticated,
560        ensureMIME(JSON_MIME),
561        function (req,res) {
562            var id = req.params.id;
563            var doc = req.body;
564            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
565            var hr;
566            if ( typeof doc.secret === 'undefined' ) {
567                hr = randomToken()
568                .handle({
569                    201: function(token) {
570                        doc.secret = token;
571                        return putDocument(id,rev,'SurveyRun',doc);
572                    }
573                });
574            } else {
575                hr = putDocument(id,rev,'SurveyRun',doc);
576            }
577            hr.handle(res.send.bind(res));
578        });
579    app.delete('/api/surveyRuns/:id',
580        ensureAuthenticated,
581        ensureMIME(JSON_MIME),
582        makeDocDel_id('SurveyRun'));
583    app.get('/api/surveyRuns/:id/responses',
584        ensureAuthenticated,
585        ensureMIME(JSON_MIME),
586        function(req,res) {
587            var id = req.params.id;
588            getResponsesBySurveyRunId(id)
589            .handle(res.send.bind(res));
590        });
591    app.get('/api/surveyRuns/:id/responses.csv',
592        ensureAuthenticated,
593        ensureMIME(CSV_MIME),
594        function(req, res) {
595            var id = req.params.id;
596            getResponsesBySurveyRunId(id)
597            .handle({
598                200: function(responses) {
599                    var flatResponses = responsesToVariables(responses);
600                    res.set({
601                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
602                    });
603                    res.status(200);
604                    writeObjectsToCSVStream(flatResponses, res);
605                    res.end();
606                },
607                default: res.send.bind(res)
608            });
609        });
610   
611    // Responses
612    function getResponsesBySurveyRunId(surveyRunId) {
613        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
614        return HTTPResult.fromResponsePromise(couch.get(url).response,
615                                              handleUnknownError)
616        .handle({
617            200: handleRowValues,
618            default: handleUnknownResponse
619        });
620    }
621    app.get('/api/responses',
622        ensureAuthenticated,
623        ensureMIME(JSON_MIME),
624        function(req,res){
625            var hr;
626            if ( 'surveyRunId' in req.query ) {
627                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
628            } else {
629                hr = getDocumentsOfType('Response');
630            }
631            hr.handle(res.send.bind(res));
632        });
633    app.get('/api/responses/:id',
634        ensureAuthenticated,
635        ensureMIME(JSON_MIME),
636        makeDocGet_id('Response'));
637    app.post('/api/responses',
638        ensureAuthenticated,
639        ensureMIME(JSON_MIME),
640        function (req,res) {
641            var doc = req.body;
642            randomToken()
643            .handle({
644                201: function(token) {
645                    doc.secret = token;
646                    return postDocument('Response',doc);
647                }
648            })
649            .handle(res.send.bind(res));
650        });
651    app.put('/api/responses/:id',
652        ensureAuthenticated,
653        ensureMIME(JSON_MIME),
654        function (req,res) {
655            var id = req.params.id;
656            var doc = req.body;
657            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
658            var hr;
659            if ( typeof doc.secret === 'undefined' ) {
660                hr = randomToken()
661                .handle({
662                    201: function(token) {
663                        doc.secret = token;
664                        return putDocument(id,rev,'Response',doc);
665                    }
666                });
667            } else {
668                hr = putDocument(id,rev,'Response',doc);
669            }
670            hr.handle(res.send.bind(res));
671        });
672    app.delete('/api/responses/:id',
673        ensureAuthenticated,
674        ensureMIME(JSON_MIME),
675        makeDocDel_id('Response'));
676
677    //respondents api
678    function isSurveyRunRunning(surveyRun) {
679        var now = new Date();
680        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
681        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
682        return afterStart && beforeEnd;
683    }
684    app.get('/api/open/responses/:id',
685        ensureMIME(JSON_MIME),
686        function(req,res) {
687            var id = req.params.id;
688            var rev = etags.parse(req.header('If-Non-Match'))[0];
689            var secret = req.query.secret;
690            getDocument(id,rev,'Response')
691            .handle({
692                200: function(response) {
693                    if ( response.secret === secret ) {
694                        return getDocument(response.surveyRunId,[],'SurveyRun')
695                        .handle({
696                            200: function(surveyRun) {
697                                if ( !isSurveyRunRunning(surveyRun) ) {
698                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
699                                } else {
700                                    response._surveyRun = surveyRun;
701                                    return response;
702                                }
703                            }
704                        });
705                    } else {
706                        return new HTTPResult(403,{error: "Wrong secret for response"});
707                    }
708                }
709            })
710            .handle(res.send.bind(res));
711        });
712    app.post('/api/open/responses',
713        ensureMIME(JSON_MIME),
714        function(req,res) {
715            var secret = req.query.secret;
716            var response = req.body;
717            delete response._surveyRun;
718            getDocument(response.surveyRunId,[],'SurveyRun')
719            .handle({
720                200: function(surveyRun) {
721                    if ( surveyRun.secret !== secret ) {
722                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
723                    } else if ( !isSurveyRunRunning(surveyRun) ) {
724                        return new HTTPResult(404,{error:"Survey is not running anymore."});
725                    } else if ( surveyRun.mode === 'closed' ) {
726                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
727                    } else {
728                        return randomToken()
729                        .handle({
730                            201: function(token) {
731                                response.secret = token;
732                                return postDocument('Response',response)
733                                .handle({
734                                    201: function(doc){
735                                        doc._surveyRun = surveyRun;
736                                        return doc;
737                                    }
738                                });
739                            }
740                        });
741                    }
742                }
743            })
744            .handle(res.send.bind(res));
745        });
746    app.put('/api/open/responses/:id',
747        ensureMIME(JSON_MIME),
748        function(req,res){
749            var id = req.params.id;
750            var doc = req.body;
751            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
752            var secret = req.query.secret || doc.secret;
753            delete doc._surveyRun;
754            getDocument(id,[],'Response')
755            .handle({
756                200: function(prev) {
757                    if ( prev.secret !== secret ) {
758                        return new HTTPResult(403,{error: "Secrets are not the same."});
759                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
760                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
761                    } else {
762                        doc.secret = secret; // restore in case it got lost or was changed
763                        return getDocument(doc.surveyRunId,[],'SurveyRun')
764                        .handle({
765                            200: function(surveyRun) {
766                                if ( !isSurveyRunRunning(surveyRun) ) {
767                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
768                                } else {
769                                    return putDocument(id,rev,'Response',doc)
770                                    .handle({
771                                        200: function(doc) {
772                                            doc._surveyRun = surveyRun;
773                                            return new HTTPResult(201,doc);
774                                        }
775                                    });
776                                }
777                            }
778                        });
779                    }
780                }
781            })
782            .handle(res.send.bind(res));
783        });
784    app.delete('/api/open/responses/:id',
785        ensureMIME(JSON_MIME),
786        function(req,res){
787            var id = req.params.id;
788            var doc = req.body;
789            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
790            var secret = req.query.secret || doc.secret;
791            delete doc._surveyRun;
792            getDocument(id,[],'Response')
793            .handle({
794                200: function(prev) {
795                    if ( prev.secret !== secret ) {
796                        return new HTTPResult(403,{error: "Secrets are not the same."});
797                    } else {
798                        return getDocument(doc.surveyRunId,[],'SurveyRun')
799                        .handle({
800                            200: function(surveyRun) {
801                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
802                                    return deleteDocument(id,rev,doc);
803                                } else {
804                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
805                                }
806                            }
807                        });
808                    }
809                }
810            })
811            .handle(res.send.bind(res));
812        });
813
814    // uuids
815    app.get('/api/uuids',
816        ensureAuthenticated,
817        ensureMIME(JSON_MIME),
818        function(req,res){
819            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
820            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
821                                           handleUnknownError)
822            .handle({
823                200: function(res) {
824                    return res.uuids;
825                },
826                default: handleUnknownResponse
827            })
828            .handle(res.send.bind(res));
829        });
830
831    return app;
832}
833
834function responsesToVariables(responses) {
835    return _.map(responses, responseToVariables);
836}
837
838function responseToVariables(response) {
839    var result = flattenObject(response.answers);
840    return result;
841}
842
843function flattenObject(value) {
844    var result = {};
845    (function agg(val,key,res){
846        if ( _.isObject(val) ) {
847            var keys = _.keys(val);
848            // FIXME : dirty hack for questions with only one input
849            if ( keys.length === 1 ) {
850                agg(val[keys[0]],key,res);
851            } else {
852                _.forEach(val, function(v,k){
853                    agg(v,(key ? key+'.' : '')+k,res);
854                });
855            }
856        } else if ( _.isArray(val) ) {
857            // FIXME : dirty hack for questions with only one input
858            if ( val.length === 1 ) {
859                agg(val[0],key,res);
860            } else {
861                _.forEach(val, function(v,i){
862                    agg(v,(key ? key+'.' : '')+i,res);
863                });
864            }
865        } else {
866            res[key] = val;
867        }
868    })(value,null,result);
869    return result;
870}
871
872function writeObjectsToCSVStream(objects, stream, prelude) {
873    var keys = _.chain(objects)
874                .map(_.keys)
875                .flatten()
876                .uniq()
877                .value();
878    var idxs = {};
879    _.forEach(keys, function(key,idx){
880        idxs[key] = idx;
881    });
882    var writer = new CSV.CsvWriter(stream);
883    if ( prelude ) {
884        _.forEach(prelude, function(val,key){
885            writer.writeRecord([key,val]);
886        });
887    }
888    writer.writeRecord(keys);
889    _.forEach(objects, function(obj){
890        var row = [];
891        _.forEach(obj, function(val,key){
892            row[idxs[key]] = val;
893        });
894        writer.writeRecord(row);
895    });
896}
897
898function randomToken() {
899    var result = new HTTPResult();
900    cryptoken(8)
901    .then(function(token){
902        result.set(201,token);
903    }, function(ex){
904        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
905    });
906    return result;
907}
Note: See TracBrowser for help on using the repository browser.