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

Last change on this file since 497 was 497, checked in by hendrikvanantwerpen, 11 years ago
  • Better names for db backup and sync scripts.
  • Added SurveyRun? deletion in UI and prevent on server when a run has replies.
File size: 34.0 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        function(req,res) {
625            var id = req.params.id;
626            var doc = req.body;
627            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
628            getResponsesBySurveyRunId(id)
629            .handle({
630                200: function(responses) {
631                    if ( responses.length > 0 ) {
632                        return new HTTPResult(403,{error:"Cannot delete run that has responses."});
633                    } else {
634                        return deleteDocument(id,rev);
635                    }
636                }
637            }).handle(res.send.bind(res));
638        });
639    app.get('/api/surveyRuns/:id/responses',
640        ensureAuthenticated,
641        ensureMIME(JSON_MIME),
642        function(req,res) {
643            var id = req.params.id;
644            getResponsesBySurveyRunId(id)
645            .handle(res.send.bind(res));
646        });
647    app.get('/api/surveyRuns/:id/responses.csv',
648        ensureAuthenticated,
649        ensureMIME(CSV_MIME),
650        function(req, res) {
651            var id = req.params.id;
652            getResponsesBySurveyRunId(id)
653            .handle({
654                200: function(responses) {
655                    var flatResponses = responsesToVariables(responses);
656                    res.set({
657                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
658                    });
659                    res.status(200);
660                    writeObjectsToCSVStream(flatResponses, res);
661                    res.end();
662                },
663                default: res.send.bind(res)
664            });
665        });
666   
667    // Responses
668    function getResponsesBySurveyRunId(surveyRunId) {
669        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
670        return HTTPResult.fromResponsePromise(couch.get(url).response,
671                                              handleUnknownError)
672        .handle({
673            200: handleRowValues,
674            default: handleUnknownResponse
675        });
676    }
677    app.get('/api/responses',
678        ensureAuthenticated,
679        ensureMIME(JSON_MIME),
680        function(req,res){
681            var hr;
682            if ( 'surveyRunId' in req.query ) {
683                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
684            } else {
685                hr = getDocumentsOfType('Response');
686            }
687            hr.handle(res.send.bind(res));
688        });
689    app.get('/api/responses/:id',
690        ensureAuthenticated,
691        ensureMIME(JSON_MIME),
692        makeDocGet_id('Response'));
693    app.post('/api/responses',
694        ensureAuthenticated,
695        ensureMIME(JSON_MIME),
696        function (req,res) {
697            var doc = req.body;
698            randomToken()
699            .handle({
700                201: function(token) {
701                    doc.secret = token;
702                    return postDocument('Response',doc);
703                }
704            })
705            .handle(res.send.bind(res));
706        });
707    app.put('/api/responses/:id',
708        ensureAuthenticated,
709        ensureMIME(JSON_MIME),
710        function (req,res) {
711            var id = req.params.id;
712            var doc = req.body;
713            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
714            var hr;
715            if ( typeof doc.secret === 'undefined' ) {
716                hr = randomToken()
717                .handle({
718                    201: function(token) {
719                        doc.secret = token;
720                        return putDocument(id,rev,'Response',doc);
721                    }
722                });
723            } else {
724                hr = putDocument(id,rev,'Response',doc);
725            }
726            hr.handle(res.send.bind(res));
727        });
728    app.delete('/api/responses/:id',
729        ensureAuthenticated,
730        ensureMIME(JSON_MIME),
731        makeDocDel_id('Response'));
732
733    //respondents api
734    function isSurveyRunRunning(surveyRun) {
735        var now = new Date();
736        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
737        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
738        return afterStart && beforeEnd;
739    }
740    app.get('/api/open/responses/:id',
741        ensureMIME(JSON_MIME),
742        function(req,res) {
743            var id = req.params.id;
744            var rev = etags.parse(req.header('If-Non-Match'))[0];
745            var secret = req.query.secret;
746            getDocument(id,rev,'Response')
747            .handle({
748                200: function(response) {
749                    if ( response.secret === secret ) {
750                        return getDocument(response.surveyRunId,[],'SurveyRun')
751                        .handle({
752                            200: function(surveyRun) {
753                                if ( !isSurveyRunRunning(surveyRun) ) {
754                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
755                                } else {
756                                    response._surveyRun = surveyRun;
757                                    return response;
758                                }
759                            }
760                        });
761                    } else {
762                        return new HTTPResult(403,{error: "Wrong secret for response"});
763                    }
764                }
765            })
766            .handle(res.send.bind(res));
767        });
768    app.post('/api/open/responses',
769        ensureMIME(JSON_MIME),
770        function(req,res) {
771            var secret = req.query.secret;
772            var response = req.body;
773            delete response._surveyRun;
774            getDocument(response.surveyRunId,[],'SurveyRun')
775            .handle({
776                200: function(surveyRun) {
777                    if ( surveyRun.secret !== secret ) {
778                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
779                    } else if ( !isSurveyRunRunning(surveyRun) ) {
780                        return new HTTPResult(404,{error:"Survey is not running anymore."});
781                    } else if ( surveyRun.mode === 'closed' ) {
782                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
783                    } else {
784                        return randomToken()
785                        .handle({
786                            201: function(token) {
787                                response.secret = token;
788                                return postDocument('Response',response)
789                                .handle({
790                                    201: function(doc){
791                                        doc._surveyRun = surveyRun;
792                                        return doc;
793                                    }
794                                });
795                            }
796                        });
797                    }
798                }
799            })
800            .handle(res.send.bind(res));
801        });
802    app.put('/api/open/responses/:id',
803        ensureMIME(JSON_MIME),
804        function(req,res){
805            var id = req.params.id;
806            var doc = req.body;
807            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
808            var secret = req.query.secret || doc.secret;
809            delete doc._surveyRun;
810            getDocument(id,[],'Response')
811            .handle({
812                200: function(prev) {
813                    if ( prev.secret !== secret ) {
814                        return new HTTPResult(403,{error: "Secrets are not the same."});
815                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
816                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
817                    } else {
818                        doc.secret = secret; // restore in case it got lost or was changed
819                        return getDocument(doc.surveyRunId,[],'SurveyRun')
820                        .handle({
821                            200: function(surveyRun) {
822                                if ( !isSurveyRunRunning(surveyRun) ) {
823                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
824                                } else {
825                                    return putDocument(id,rev,'Response',doc)
826                                    .handle({
827                                        200: function(doc) {
828                                            doc._surveyRun = surveyRun;
829                                            return new HTTPResult(201,doc);
830                                        }
831                                    });
832                                }
833                            }
834                        });
835                    }
836                }
837            })
838            .handle(res.send.bind(res));
839        });
840    app.delete('/api/open/responses/:id',
841        ensureMIME(JSON_MIME),
842        function(req,res){
843            var id = req.params.id;
844            var doc = req.body;
845            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
846            var secret = req.query.secret || doc.secret;
847            delete doc._surveyRun;
848            getDocument(id,[],'Response')
849            .handle({
850                200: function(prev) {
851                    if ( prev.secret !== secret ) {
852                        return new HTTPResult(403,{error: "Secrets are not the same."});
853                    } else {
854                        return getDocument(doc.surveyRunId,[],'SurveyRun')
855                        .handle({
856                            200: function(surveyRun) {
857                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
858                                    return deleteDocument(id,rev,doc);
859                                } else {
860                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
861                                }
862                            }
863                        });
864                    }
865                }
866            })
867            .handle(res.send.bind(res));
868        });
869
870    // uuids
871    app.get('/api/uuids',
872        ensureAuthenticated,
873        ensureMIME(JSON_MIME),
874        function(req,res){
875            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
876            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
877                                           handleUnknownError)
878            .handle({
879                200: function(res) {
880                    return res.uuids;
881                },
882                default: handleUnknownResponse
883            })
884            .handle(res.send.bind(res));
885        });
886
887    return app;
888}
889
890function responsesToVariables(responses) {
891    return _.map(responses, responseToVariables);
892}
893
894function responseToVariables(response) {
895    var result = flattenObject(response.answers);
896    return result;
897}
898
899function flattenObject(value) {
900    var result = {};
901    (function agg(val,key,res){
902        if ( _.isObject(val) ) {
903            var keys = _.keys(val);
904            // FIXME : dirty hack for questions with only one input
905            if ( keys.length === 1 ) {
906                agg(val[keys[0]],key,res);
907            } else {
908                _.forEach(val, function(v,k){
909                    agg(v,(key ? key+'.' : '')+k,res);
910                });
911            }
912        } else if ( _.isArray(val) ) {
913            // FIXME : dirty hack for questions with only one input
914            if ( val.length === 1 ) {
915                agg(val[0],key,res);
916            } else {
917                _.forEach(val, function(v,i){
918                    agg(v,(key ? key+'.' : '')+i,res);
919                });
920            }
921        } else {
922            res[key] = val;
923        }
924    })(value,null,result);
925    return result;
926}
927
928function writeObjectsToCSVStream(objects, stream, prelude) {
929    var keys = _.chain(objects)
930                .map(_.keys)
931                .flatten()
932                .uniq()
933                .value();
934    var idxs = {};
935    _.forEach(keys, function(key,idx){
936        idxs[key] = idx;
937    });
938    var writer = new CSV.CsvWriter(stream);
939    if ( prelude ) {
940        _.forEach(prelude, function(val,key){
941            writer.writeRecord([key,val]);
942        });
943    }
944    writer.writeRecord(keys);
945    _.forEach(objects, function(obj){
946        var row = [];
947        _.forEach(obj, function(val,key){
948            row[idxs[key]] = val;
949        });
950        writer.writeRecord(row);
951    });
952}
953
954function randomToken() {
955    var result = new HTTPResult();
956    cryptoken(8)
957    .then(function(token){
958        result.set(201,token);
959    }, function(ex){
960        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
961    });
962    return result;
963}
Note: See TracBrowser for help on using the repository browser.