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

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