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

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