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

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