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

Last change on this file since 507 was 507, checked in by hendrikvanantwerpen, 11 years ago

Check that subcodes are unique.

File size: 36.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 ( !areDocsPublished(doc.questions) || isDocPublished(doc) ) {
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            var hr;
613            if ( !areDocsUnique(doc.questions) ) {
614                new HTTPResult(400,{error:"Survey contains duplicate questions."})
615                .handle(res.send.bind(res));
616            } else if ( !areDocsPublished(doc.questions) || isDocPublished(doc) ) {
617                hr = new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."});
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 flatResponses = responsesToVariables(responses);
721                    res.set({
722                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
723                    });
724                    res.status(200);
725                    writeObjectsToCSVStream(flatResponses, res);
726                    res.end();
727                },
728                default: res.send.bind(res)
729            });
730        });
731   
732    // Responses
733    function getResponsesBySurveyRunId(surveyRunId) {
734        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
735        return HTTPResult.fromResponsePromise(couch.get(url).response,
736                                              handleUnknownError)
737        .handle({
738            200: handleRowValues,
739            default: handleUnknownResponse
740        });
741    }
742    app.get('/api/responses',
743        ensureAuthenticated,
744        ensureMIME(JSON_MIME),
745        function(req,res){
746            var hr;
747            if ( 'surveyRunId' in req.query ) {
748                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
749            } else {
750                hr = getDocumentsOfType('Response');
751            }
752            hr.handle(res.send.bind(res));
753        });
754    app.get('/api/responses/:id',
755        ensureAuthenticated,
756        ensureMIME(JSON_MIME),
757        makeDocGet_id('Response'));
758    app.post('/api/responses',
759        ensureAuthenticated,
760        ensureMIME(JSON_MIME),
761        function (req,res) {
762            var doc = req.body;
763            randomToken()
764            .handle({
765                201: function(token) {
766                    doc.secret = token;
767                    return postDocument('Response',doc);
768                }
769            })
770            .handle(res.send.bind(res));
771        });
772    app.put('/api/responses/:id',
773        ensureAuthenticated,
774        ensureMIME(JSON_MIME),
775        function (req,res) {
776            var id = req.params.id;
777            var doc = req.body;
778            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
779            var hr;
780            if ( typeof doc.secret === 'undefined' ) {
781                hr = randomToken()
782                .handle({
783                    201: function(token) {
784                        doc.secret = token;
785                        return putDocument(id,rev,'Response',doc);
786                    }
787                });
788            } else {
789                hr = putDocument(id,rev,'Response',doc);
790            }
791            hr.handle(res.send.bind(res));
792        });
793    app.delete('/api/responses/:id',
794        ensureAuthenticated,
795        ensureMIME(JSON_MIME),
796        makeDocDel_id('Response'));
797
798    //respondents api
799    function isSurveyRunRunning(surveyRun) {
800        var now = new Date();
801        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
802        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
803        return afterStart && beforeEnd;
804    }
805    app.get('/api/open/responses/:id',
806        ensureMIME(JSON_MIME),
807        function(req,res) {
808            var id = req.params.id;
809            var rev = etags.parse(req.header('If-Non-Match'))[0];
810            var secret = req.query.secret;
811            getDocument(id,rev,'Response')
812            .handle({
813                200: function(response) {
814                    if ( response.secret === secret ) {
815                        return getDocument(response.surveyRunId,[],'SurveyRun')
816                        .handle({
817                            200: function(surveyRun) {
818                                if ( !isSurveyRunRunning(surveyRun) ) {
819                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
820                                } else {
821                                    response._surveyRun = surveyRun;
822                                    return response;
823                                }
824                            }
825                        });
826                    } else {
827                        return new HTTPResult(403,{error: "Wrong secret for response"});
828                    }
829                }
830            })
831            .handle(res.send.bind(res));
832        });
833    app.post('/api/open/responses',
834        ensureMIME(JSON_MIME),
835        function(req,res) {
836            var secret = req.query.secret;
837            var response = req.body;
838            delete response._surveyRun;
839            getDocument(response.surveyRunId,[],'SurveyRun')
840            .handle({
841                200: function(surveyRun) {
842                    if ( surveyRun.secret !== secret ) {
843                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
844                    } else if ( !isSurveyRunRunning(surveyRun) ) {
845                        return new HTTPResult(404,{error:"Survey is not running anymore."});
846                    } else if ( surveyRun.mode === 'closed' ) {
847                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
848                    } else {
849                        return randomToken()
850                        .handle({
851                            201: function(token) {
852                                response.secret = token;
853                                return postDocument('Response',response)
854                                .handle({
855                                    201: function(doc){
856                                        doc._surveyRun = surveyRun;
857                                        return doc;
858                                    }
859                                });
860                            }
861                        });
862                    }
863                }
864            })
865            .handle(res.send.bind(res));
866        });
867    app.put('/api/open/responses/:id',
868        ensureMIME(JSON_MIME),
869        function(req,res){
870            var id = req.params.id;
871            var doc = req.body;
872            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
873            var secret = req.query.secret || doc.secret;
874            delete doc._surveyRun;
875            getDocument(id,[],'Response')
876            .handle({
877                200: function(prev) {
878                    if ( prev.secret !== secret ) {
879                        return new HTTPResult(403,{error: "Secrets are not the same."});
880                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
881                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
882                    } else {
883                        doc.secret = secret; // restore in case it got lost or was changed
884                        return getDocument(doc.surveyRunId,[],'SurveyRun')
885                        .handle({
886                            200: function(surveyRun) {
887                                if ( !isSurveyRunRunning(surveyRun) ) {
888                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
889                                } else {
890                                    return putDocument(id,rev,'Response',doc)
891                                    .handle({
892                                        200: function(doc) {
893                                            doc._surveyRun = surveyRun;
894                                            return new HTTPResult(201,doc);
895                                        }
896                                    });
897                                }
898                            }
899                        });
900                    }
901                }
902            })
903            .handle(res.send.bind(res));
904        });
905    app.delete('/api/open/responses/:id',
906        ensureMIME(JSON_MIME),
907        function(req,res){
908            var id = req.params.id;
909            var doc = req.body;
910            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
911            var secret = req.query.secret || doc.secret;
912            delete doc._surveyRun;
913            getDocument(id,[],'Response')
914            .handle({
915                200: function(prev) {
916                    if ( prev.secret !== secret ) {
917                        return new HTTPResult(403,{error: "Secrets are not the same."});
918                    } else {
919                        return getDocument(doc.surveyRunId,[],'SurveyRun')
920                        .handle({
921                            200: function(surveyRun) {
922                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
923                                    return deleteDocument(id,rev,doc);
924                                } else {
925                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
926                                }
927                            }
928                        });
929                    }
930                }
931            })
932            .handle(res.send.bind(res));
933        });
934
935    // uuids
936    app.get('/api/uuids',
937        ensureAuthenticated,
938        ensureMIME(JSON_MIME),
939        function(req,res){
940            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
941            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
942                                           handleUnknownError)
943            .handle({
944                200: function(res) {
945                    return res.uuids;
946                },
947                default: handleUnknownResponse
948            })
949            .handle(res.send.bind(res));
950        });
951
952    return app;
953}
954
955function responsesToVariables(responses) {
956    return _.map(responses, responseToVariables);
957}
958
959function responseToVariables(response) {
960    var result = flattenObject(response.answers);
961    return result;
962}
963
964function flattenObject(value) {
965    var result = {};
966    (function agg(val,key,res){
967        if ( _.isObject(val) ) {
968            var keys = _.keys(val);
969            // FIXME : dirty hack for questions with only one input
970            if ( keys.length === 1 ) {
971                agg(val[keys[0]],key,res);
972            } else {
973                _.forEach(val, function(v,k){
974                    agg(v,(key ? key+'.' : '')+k,res);
975                });
976            }
977        } else if ( _.isArray(val) ) {
978            // FIXME : dirty hack for questions with only one input
979            if ( val.length === 1 ) {
980                agg(val[0],key,res);
981            } else {
982                _.forEach(val, function(v,i){
983                    agg(v,(key ? key+'.' : '')+i,res);
984                });
985            }
986        } else {
987            res[key] = val;
988        }
989    })(value,null,result);
990    return result;
991}
992
993function writeObjectsToCSVStream(objects, stream, prelude) {
994    var keys = _.chain(objects)
995                .map(_.keys)
996                .flatten()
997                .uniq()
998                .value();
999    var idxs = {};
1000    _.forEach(keys, function(key,idx){
1001        idxs[key] = idx;
1002    });
1003    var writer = new CSV.CsvWriter(stream);
1004    if ( prelude ) {
1005        _.forEach(prelude, function(val,key){
1006            writer.writeRecord([key,val]);
1007        });
1008    }
1009    writer.writeRecord(keys);
1010    _.forEach(objects, function(obj){
1011        var row = [];
1012        _.forEach(obj, function(val,key){
1013            row[idxs[key]] = val;
1014        });
1015        writer.writeRecord(row);
1016    });
1017}
1018
1019function randomToken() {
1020    var result = new HTTPResult();
1021    cryptoken(8)
1022    .then(function(token){
1023        result.set(201,token);
1024    }, function(ex){
1025        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
1026    });
1027    return result;
1028}
Note: See TracBrowser for help on using the repository browser.