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

Last change on this file since 501 was 501, checked in by hendrikvanantwerpen, 11 years ago
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    app.get('/api/questions',
410        ensureAuthenticated,
411        ensureMIME(JSON_MIME),
412        function(req,res) {
413            var sub = 'published' in req.query && 'published';
414            var hr;
415            if ( 'category' in req.query ) {
416                hr = getQuestionsWithCategoryAndTopic(req.query.category,
417                                                      req.query.topic,sub);
418            } else if ( 'topic' in req.query ) {
419                hr = getQuestionsWithTopic(req.query.topic,sub);
420            } else if ( 'code' in req.query ) {
421                hr = getQuestionsWithCode(req.query.code,sub);
422            } else {
423                hr = getQuestions(sub);
424            }
425            hr.handle(res.send.bind(res));
426        });
427    app.post('/api/questions',
428        ensureAuthenticated,
429        ensureMIME(JSON_MIME),
430        function (req,res) {
431            var doc = req.body;
432            getQuestionsWithCode(doc.code)
433            .handle({
434                200: function(others) {
435                    if ( others.length > 0 ) {
436                        return new HTTPResult(403,{error:"Other question with this code already exists."});
437                    } else {
438                        return postDocument('Question',doc);
439                    }
440                }
441            })
442            .handle(res.send.bind(res));
443        });
444    app.get('/api/questions/:id',
445        ensureAuthenticated,
446        ensureMIME(JSON_MIME),
447        makeDocGet_id('Question'));
448    app.put('/api/questions/:id',
449        ensureAuthenticated,
450        ensureMIME(JSON_MIME),
451        function (req,res) {
452            var id = req.params.id;
453            var doc = req.body;
454            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
455            getQuestionsWithCode(doc.code)
456            .handle({
457                200: function(others) {
458                    if ( others.length > 0 && _.some(others,function(other){ return other._id !== id; })  ) {
459                        return new HTTPResult(403,{error:"Other question with this code already exists."});
460                    } else {
461                        return putDocument(id,rev,'Question',doc);
462                    }
463                }
464            })
465            .handle(res.send.bind(res));
466        });
467    app.delete('/api/questions/:id',
468        ensureAuthenticated,
469        ensureMIME(JSON_MIME),
470        makeDocDel_id('Question'));
471
472    // Categories and topics
473    function getTopics(sub) {
474        var url = '_design/questions/_view/'+(sub||'all')+'_topics';
475        return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
476                                            handleUnknownError)
477        .handle({
478            200: function(result) {
479                return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
480            },
481            default: handleUnknownResponse
482        });
483    }
484    function getCategories(sub) {
485        var url = '_design/questions/_view/'+(sub||'all');
486        return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
487                                       handleUnknownError)
488        .handle({
489            200: function(result) {
490                return _.map(result.rows, function(item) {
491                    return { name:item.key[0], count:item.value };
492                });
493            },
494            default: handleUnknownResponse
495        });
496    }
497    function getTopicsWithCategory(category,sub) {
498        var url = '_design/questions/_view/'+(sub||'all');
499        return HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
500                                       handleUnknownError)
501        .handle({
502            200: function(result) {
503                return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
504            },
505            default: handleUnknownResponse
506        });
507    }
508    app.get('/api/categories',
509        ensureAuthenticated,
510        ensureMIME(JSON_MIME),
511        function(req,res) {
512            var sub = 'published' in req.query && 'published';
513            getCategories(sub)
514            .handle(res.send.bind(res));
515        });
516    app.get('/api/categories/:category/topics',
517        ensureAuthenticated,
518        ensureMIME(JSON_MIME),
519        function(req,res) {
520            var category = req.params.category;
521            var sub = 'published' in req.query && 'published';
522            getTopicsWithCategory(category,sub)
523            .handle(res.send.bind(res));
524        });
525    app.get('/api/topics',
526        ensureAuthenticated,
527        ensureMIME(JSON_MIME),
528        function(req,res) {
529            var sub = 'published' in req.query && 'published';
530            var hr;
531            if ( 'category' in req.query ) {
532                hr = getTopicsWithCategory(req.query.category,sub);
533            } else {
534                hr = getTopics(sub);
535            }
536            hr.handle(res.send.bind(res));
537        });
538
539    // Surveys
540    app.get('/api/surveys',
541        ensureAuthenticated,
542        ensureMIME(JSON_MIME),
543        function(req,res) {
544            var url;
545            if ( 'drafts' in req.query ) {
546                url = '_design/surveys/_view/drafts';
547            } else if ( 'published' in req.query ) {
548                url = '_design/surveys/_view/published';
549            } else {
550                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
551            }
552            HTTPResult.fromResponsePromise(couch.get(url).response,
553                                           handleUnknownError)
554            .handle({
555                200: function(result) {
556                    return _.map(result.rows, function(item) { return item.value; });
557                },
558                default: handleUnknownResponse
559            })
560            .handle(res.send.bind(res));
561        });
562    app.post('/api/surveys',
563        ensureAuthenticated,
564        ensureMIME(JSON_MIME),
565        function(req,res) {
566            var doc = req.body;
567            var hr;
568            if ( !areDocsUnique(doc.questions) ) {
569                hr = new HTTPResult(400,{error:"Survey contains duplicate questions."});
570            } else if ( !areDocsPublished(doc.questions) || isDocPublished(doc) ) {
571                hr = new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."});
572            } else {
573                hr = postDocument('Survey',doc);
574            }
575            hr.handle(res.send.bind(res));
576        });
577    app.get('/api/surveys/:id',
578        ensureAuthenticated,
579        ensureMIME(JSON_MIME),
580        makeDocGet_id('Survey'));
581    app.put('/api/surveys/:id',
582        ensureAuthenticated,
583        ensureMIME(JSON_MIME),
584        function(req,res) {
585            var id = req.params.id;
586            var doc = req.body;
587            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
588            var hr;
589            if ( !areDocsUnique(doc.questions) ) {
590                new HTTPResult(400,{error:"Survey contains duplicate questions."})
591                .handle(res.send.bind(res));
592            } else if ( !areDocsPublished(doc.questions) || isDocPublished(doc) ) {
593                hr = new HTTPResult(400,{error:"Cannot publish Survey with unpublished questions."});
594            } else {
595                putDocument(id,rev,'Survey',doc)
596                .handle({
597                    201: function(doc) {
598                        res.set({
599                            'ETag': etags.format([doc._rev])
600                        }).send(201, doc);
601                    },
602                    default: res.send.bind(res)
603                });
604            }
605        });
606    app.delete('/api/surveys/:id',
607        ensureAuthenticated,
608        ensureMIME(JSON_MIME),
609        makeDocDel_id('Survey'));
610
611    // SurveyRuns
612    app.get('/api/surveyRuns',
613        ensureAuthenticated,
614        ensureMIME(JSON_MIME),
615        makeDocsGet('SurveyRun'));
616    app.post('/api/surveyRuns',
617        ensureAuthenticated,
618        ensureMIME(JSON_MIME),
619        function(req,res) {
620            var doc = req.body;
621            var hr;
622            if ( !isDocPublished(doc.survey) ) {
623                hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
624            } else {
625                hr = randomToken()
626                .handle({
627                    201: function(token) {
628                        doc.secret = token;
629                        return postDocument('SurveyRun',doc);
630                    }
631                });
632            }
633            hr.handle(res.send.bind(res));
634        });
635    app.get('/api/surveyRuns/:id',
636        ensureAuthenticated,
637        ensureMIME(JSON_MIME),
638        makeDocGet_id('SurveyRun'));
639    app.put('/api/surveyRuns/:id',
640        ensureAuthenticated,
641        ensureMIME(JSON_MIME),
642        function (req,res) {
643            var id = req.params.id;
644            var doc = req.body;
645            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
646            var hr;
647            if ( !isDocPublished(doc.survey) ) {
648                hr = new HTTPResult(400,{error:"Cannot run unpublished survey."});
649            } else if ( typeof doc.secret === 'undefined' ) {
650                hr = randomToken()
651                .handle({
652                    201: function(token) {
653                        doc.secret = token;
654                        return putDocument(id,rev,'SurveyRun',doc);
655                    }
656                });
657            } else {
658                hr = putDocument(id,rev,'SurveyRun',doc);
659            }
660            hr.handle(res.send.bind(res));
661        });
662    app.delete('/api/surveyRuns/:id',
663        ensureAuthenticated,
664        ensureMIME(JSON_MIME),
665        function(req,res) {
666            var id = req.params.id;
667            var doc = req.body;
668            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
669            getResponsesBySurveyRunId(id)
670            .handle({
671                200: function(responses) {
672                    if ( responses.length > 0 ) {
673                        return new HTTPResult(403,{error:"Cannot delete run that has responses."});
674                    } else {
675                        return deleteDocument(id,rev);
676                    }
677                }
678            }).handle(res.send.bind(res));
679        });
680    app.get('/api/surveyRuns/:id/responses',
681        ensureAuthenticated,
682        ensureMIME(JSON_MIME),
683        function(req,res) {
684            var id = req.params.id;
685            getResponsesBySurveyRunId(id)
686            .handle(res.send.bind(res));
687        });
688    app.get('/api/surveyRuns/:id/responses.csv',
689        ensureAuthenticated,
690        ensureMIME(CSV_MIME),
691        function(req, res) {
692            var id = req.params.id;
693            getResponsesBySurveyRunId(id)
694            .handle({
695                200: function(responses) {
696                    var flatResponses = responsesToVariables(responses);
697                    res.set({
698                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
699                    });
700                    res.status(200);
701                    writeObjectsToCSVStream(flatResponses, res);
702                    res.end();
703                },
704                default: res.send.bind(res)
705            });
706        });
707   
708    // Responses
709    function getResponsesBySurveyRunId(surveyRunId) {
710        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
711        return HTTPResult.fromResponsePromise(couch.get(url).response,
712                                              handleUnknownError)
713        .handle({
714            200: handleRowValues,
715            default: handleUnknownResponse
716        });
717    }
718    app.get('/api/responses',
719        ensureAuthenticated,
720        ensureMIME(JSON_MIME),
721        function(req,res){
722            var hr;
723            if ( 'surveyRunId' in req.query ) {
724                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
725            } else {
726                hr = getDocumentsOfType('Response');
727            }
728            hr.handle(res.send.bind(res));
729        });
730    app.get('/api/responses/:id',
731        ensureAuthenticated,
732        ensureMIME(JSON_MIME),
733        makeDocGet_id('Response'));
734    app.post('/api/responses',
735        ensureAuthenticated,
736        ensureMIME(JSON_MIME),
737        function (req,res) {
738            var doc = req.body;
739            randomToken()
740            .handle({
741                201: function(token) {
742                    doc.secret = token;
743                    return postDocument('Response',doc);
744                }
745            })
746            .handle(res.send.bind(res));
747        });
748    app.put('/api/responses/:id',
749        ensureAuthenticated,
750        ensureMIME(JSON_MIME),
751        function (req,res) {
752            var id = req.params.id;
753            var doc = req.body;
754            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
755            var hr;
756            if ( typeof doc.secret === 'undefined' ) {
757                hr = randomToken()
758                .handle({
759                    201: function(token) {
760                        doc.secret = token;
761                        return putDocument(id,rev,'Response',doc);
762                    }
763                });
764            } else {
765                hr = putDocument(id,rev,'Response',doc);
766            }
767            hr.handle(res.send.bind(res));
768        });
769    app.delete('/api/responses/:id',
770        ensureAuthenticated,
771        ensureMIME(JSON_MIME),
772        makeDocDel_id('Response'));
773
774    //respondents api
775    function isSurveyRunRunning(surveyRun) {
776        var now = new Date();
777        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
778        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
779        return afterStart && beforeEnd;
780    }
781    app.get('/api/open/responses/:id',
782        ensureMIME(JSON_MIME),
783        function(req,res) {
784            var id = req.params.id;
785            var rev = etags.parse(req.header('If-Non-Match'))[0];
786            var secret = req.query.secret;
787            getDocument(id,rev,'Response')
788            .handle({
789                200: function(response) {
790                    if ( response.secret === secret ) {
791                        return getDocument(response.surveyRunId,[],'SurveyRun')
792                        .handle({
793                            200: function(surveyRun) {
794                                if ( !isSurveyRunRunning(surveyRun) ) {
795                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
796                                } else {
797                                    response._surveyRun = surveyRun;
798                                    return response;
799                                }
800                            }
801                        });
802                    } else {
803                        return new HTTPResult(403,{error: "Wrong secret for response"});
804                    }
805                }
806            })
807            .handle(res.send.bind(res));
808        });
809    app.post('/api/open/responses',
810        ensureMIME(JSON_MIME),
811        function(req,res) {
812            var secret = req.query.secret;
813            var response = req.body;
814            delete response._surveyRun;
815            getDocument(response.surveyRunId,[],'SurveyRun')
816            .handle({
817                200: function(surveyRun) {
818                    if ( surveyRun.secret !== secret ) {
819                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
820                    } else if ( !isSurveyRunRunning(surveyRun) ) {
821                        return new HTTPResult(404,{error:"Survey is not running anymore."});
822                    } else if ( surveyRun.mode === 'closed' ) {
823                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
824                    } else {
825                        return randomToken()
826                        .handle({
827                            201: function(token) {
828                                response.secret = token;
829                                return postDocument('Response',response)
830                                .handle({
831                                    201: function(doc){
832                                        doc._surveyRun = surveyRun;
833                                        return doc;
834                                    }
835                                });
836                            }
837                        });
838                    }
839                }
840            })
841            .handle(res.send.bind(res));
842        });
843    app.put('/api/open/responses/:id',
844        ensureMIME(JSON_MIME),
845        function(req,res){
846            var id = req.params.id;
847            var doc = req.body;
848            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
849            var secret = req.query.secret || doc.secret;
850            delete doc._surveyRun;
851            getDocument(id,[],'Response')
852            .handle({
853                200: function(prev) {
854                    if ( prev.secret !== secret ) {
855                        return new HTTPResult(403,{error: "Secrets are not the same."});
856                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
857                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
858                    } else {
859                        doc.secret = secret; // restore in case it got lost or was changed
860                        return getDocument(doc.surveyRunId,[],'SurveyRun')
861                        .handle({
862                            200: function(surveyRun) {
863                                if ( !isSurveyRunRunning(surveyRun) ) {
864                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
865                                } else {
866                                    return putDocument(id,rev,'Response',doc)
867                                    .handle({
868                                        200: function(doc) {
869                                            doc._surveyRun = surveyRun;
870                                            return new HTTPResult(201,doc);
871                                        }
872                                    });
873                                }
874                            }
875                        });
876                    }
877                }
878            })
879            .handle(res.send.bind(res));
880        });
881    app.delete('/api/open/responses/:id',
882        ensureMIME(JSON_MIME),
883        function(req,res){
884            var id = req.params.id;
885            var doc = req.body;
886            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
887            var secret = req.query.secret || doc.secret;
888            delete doc._surveyRun;
889            getDocument(id,[],'Response')
890            .handle({
891                200: function(prev) {
892                    if ( prev.secret !== secret ) {
893                        return new HTTPResult(403,{error: "Secrets are not the same."});
894                    } else {
895                        return getDocument(doc.surveyRunId,[],'SurveyRun')
896                        .handle({
897                            200: function(surveyRun) {
898                                if ( surveyRun.respondentCanDeleteOwnResponse === true ) {
899                                    return deleteDocument(id,rev,doc);
900                                } else {
901                                    return new HTTPResult(403,{error:"Not allowed to delete response."});
902                                }
903                            }
904                        });
905                    }
906                }
907            })
908            .handle(res.send.bind(res));
909        });
910
911    // uuids
912    app.get('/api/uuids',
913        ensureAuthenticated,
914        ensureMIME(JSON_MIME),
915        function(req,res){
916            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
917            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
918                                           handleUnknownError)
919            .handle({
920                200: function(res) {
921                    return res.uuids;
922                },
923                default: handleUnknownResponse
924            })
925            .handle(res.send.bind(res));
926        });
927
928    return app;
929}
930
931function responsesToVariables(responses) {
932    return _.map(responses, responseToVariables);
933}
934
935function responseToVariables(response) {
936    var result = flattenObject(response.answers);
937    return result;
938}
939
940function flattenObject(value) {
941    var result = {};
942    (function agg(val,key,res){
943        if ( _.isObject(val) ) {
944            var keys = _.keys(val);
945            // FIXME : dirty hack for questions with only one input
946            if ( keys.length === 1 ) {
947                agg(val[keys[0]],key,res);
948            } else {
949                _.forEach(val, function(v,k){
950                    agg(v,(key ? key+'.' : '')+k,res);
951                });
952            }
953        } else if ( _.isArray(val) ) {
954            // FIXME : dirty hack for questions with only one input
955            if ( val.length === 1 ) {
956                agg(val[0],key,res);
957            } else {
958                _.forEach(val, function(v,i){
959                    agg(v,(key ? key+'.' : '')+i,res);
960                });
961            }
962        } else {
963            res[key] = val;
964        }
965    })(value,null,result);
966    return result;
967}
968
969function writeObjectsToCSVStream(objects, stream, prelude) {
970    var keys = _.chain(objects)
971                .map(_.keys)
972                .flatten()
973                .uniq()
974                .value();
975    var idxs = {};
976    _.forEach(keys, function(key,idx){
977        idxs[key] = idx;
978    });
979    var writer = new CSV.CsvWriter(stream);
980    if ( prelude ) {
981        _.forEach(prelude, function(val,key){
982            writer.writeRecord([key,val]);
983        });
984    }
985    writer.writeRecord(keys);
986    _.forEach(objects, function(obj){
987        var row = [];
988        _.forEach(obj, function(val,key){
989            row[idxs[key]] = val;
990        });
991        writer.writeRecord(row);
992    });
993}
994
995function randomToken() {
996    var result = new HTTPResult();
997    cryptoken(8)
998    .then(function(token){
999        result.set(201,token);
1000    }, function(ex){
1001        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
1002    });
1003    return result;
1004}
Note: See TracBrowser for help on using the repository browser.