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

Last change on this file since 490 was 490, checked in by hendrikvanantwerpen, 11 years ago
  • Mark content as dirty to prevent moving away from unsaved data.
  • Better change propagation from lists and our own widgets.
  • Generate notifications for errors and show correct message.
  • Moved all path/url generation to the class stores, not everywhere we use it.
  • Give user always a choice between Save and Save & Close.
  • Better refresh behaviour on form changes and saves.
  • Don't generate duplicate code error when existing object is the one you're storing.
File size: 30.2 KB
RevLine 
[475]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")
[481]10  , tv4 = require("tv4")
[487]11  , HTTPResult = require("./util/http-result")
12  , etags = require("./util/etags")
13  , cryptoken = require('./util/crypto-token')
[475]14  ;
[443]15
[463]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    }
[451]23}
[443]24
[463]25exports.App = function(settings) {
[451]26
[487]27    assertSetting("couchServerURL", settings, _.isString);
28    assertSetting("dbName", settings, _.isString);
29    var couch = new CouchDB(settings.couchServerURL,settings.dbName);
[463]30   
[481]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
[463]44    function clientPath(relativePath) {
45        return path.resolve(__dirname+'/../client/'+relativePath);
46    }
[451]47
[463]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    }));
[466]55    passport.serializeUser(function(user, done) {
56        done(null, user.username);
57    });
58    passport.deserializeUser(function(id, done) {
59        done(null, {username: id});
60    });
[451]61
[463]62    var app = express();
63    app.use(express.logger());
64    app.use(express.compress());
65    app.use(express.favicon());
66
[470]67    // cookies and session
[463]68    app.use(express.cookieParser());
69    app.use(express.session({ secret: "quasi experimental design" }));
[478]70    app.use('/api',express.bodyParser());
[463]71
[470]72    // passport
[463]73    app.use(passport.initialize());
74    app.use(passport.session());
[487]75   
76    // various middlewares
[470]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    }
[487]87    function notImplemented(req,res) {
88        res.send(501,{error:"API not implemented yet."});
89    }
[463]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
[470]102    // post to this url to login
[466]103    app.post(
104        '/api/login',
105        passport.authenticate('local'),
106        returnUser);
[463]107
[470]108    // return the info for the current logged in user
[466]109    app.get(
110        '/api/login',
[467]111        ensureAuthenticated,
[466]112        returnUser);
113
[470]114    // explicitly logout this user
[466]115    app.post(
[467]116        '/api/logout',
117        ensureAuthenticated,
118        function(req,res){
[466]119            req.logout();
120            res.send(200,{});
121        });
122
[487]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 stripAndReturnPrivates(obj) {
139        var priv = {};
140        _.each(obj||{},function(val,key){
141            if (key.substring(0,1) === '_') {
142                priv[key] = val;
143                delete obj[key];
144            }
145        });
146        return priv;
147    }
148
149    function identity(obj) { return obj; }
150    function handleUnknownResponse(status,error){
[490]151        return new HTTPResult(500,{error: error.reason});
[487]152    }
153    function handleUnknownError(error){
154        return new HTTPResult(500, {error: "Unknown error", innerError: error});
155    }
156    function handleRowValues(result) {
157        return _.map(result.rows, function(item) { return item.value; });
158    }
159    function handleRowDocs(result) {
160        return _.map(result.rows, function(item) { return item.doc; });
161    }
162
163    function getDocumentsOfType (type) {
164        var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
165        return HTTPResult.fromResponsePromise(couch.get(url).response,
166                                              handleUnknownError)
167        .handle({
168            200: handleRowValues,
169            404: function() { return {error: "Cannot find collection of type "+type}; },
170            default: handleUnknownResponse
171        });
172    }
173    function getDocument(id,rev,type) {
174        var opts = {headers:{}};
175        if (rev) {
176            opts.headers['If-Non-Match'] = '"'+rev+'"';
177        }
178        return HTTPResult.fromResponsePromise(couch.get(id,opts).response,
179                                              handleUnknownError)
180        .handle({
181            200: function(doc){
182                if ( doc.type !== type ) {
183                    return new HTTPResult(404,{error:"Document not found."});
184                } else {
185                    var priv = stripAndReturnPrivates(doc);
186                    if ( priv._rev !== rev ) {
187                        doc._id = priv._id;
188                        doc._rev = priv._rev;
189                        return doc;
190                    } else {
191                        return new HTTPResult(304);
192                    }
193                }
194            },
195            304: identity,
196            default: handleUnknownResponse
197        });
198    }
199    function putDocument(id,rev,type,doc) {
200        var priv = stripAndReturnPrivates(doc);
201        if ( doc.type === type && tv4.validateResult(doc, schema) ) {
202            var opts = rev ? {headers:{'If-Match':'"'+rev+'"'}} : {};
203            return HTTPResult.fromResponsePromise(couch.put(id,doc,opts).response,
204                                                  handleUnknownError)
205            .handle({
206                201: function(res){
207                    doc._id = res.id;
208                    doc._rev = res.rev;
209                    return doc;
210                },
211                409: function(error) {
212                    return {error: error.reason};
213                },
214                default: handleUnknownResponse
215            });
216        } else {
217            return new HTTPResult(400,{error: "Document failed schema verification."});
218        }
219    }
220    function deleteDocument(id,rev) {
221        if ( rev ) {
222            var opts = {headers:{'If-Match':'"'+rev+'"'}};
223            return HTTPResult.fromResponsePromise(couch.delete(id,opts).response,
224                                                  handleUnknownError)
225            .handle({
226                200: identity,
227                409: function(error) {
228                    return {error: error.reason};
229                },
230                default: handleUnknownResponse
231            });
232        } else {
233            return new HTTPResult(409, {error: "Cannot identify document revision to delete."});
234        }
235    }
236    function postDocument(type,doc) {
237        var priv = stripAndReturnPrivates(doc);
238        if ( doc.type === type && tv4.validateResult(doc, schema) ) {
239            return HTTPResult.fromResponsePromise(couch.post(doc).response,
240                                                  handleUnknownError)
241            .handle({
242                201: function(response) {
243                    doc._id = response.id;
244                    doc._rev = response.rev;
245                    return doc;
246                },
247                default: handleUnknownResponse
248            });
249        } else {
250            return new HTTPResult(400,{error: "Document failed schema verification."});
251        }
252    }
253
[481]254    function makeDocsGet(type) {
255        return function(req,res) {
[487]256            getDocumentsOfType(type)
257            .handle(res.send.bind(res));
[481]258        };
259    }
260    function makeDocGet_id(type) {
261        return function(req,res) {
[477]262            var id = req.params.id;
[487]263            var rev = etags.parse(req.header('If-Non-Match'))[0];
264            getDocument(id,rev,type)
265            .handle({
266                200: function(doc){
267                    res.set({
268                        'ETag': etags.format([doc._rev])
269                    }).send(200, doc);
270                },
271                default: res.send.bind(res)
[477]272            });
[481]273        };
274    }
275    function makeDocPut_id(type) {
276        return function(req,res) {
[477]277            var id = req.params.id;
[481]278            var doc = req.body;
[487]279            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
280            putDocument(id,rev,type,doc)
281            .handle({
282                201: function(doc) {
283                    res.set({
284                        'ETag': etags.format([doc._rev])
285                    }).send(201, doc);
286                },
287                default: res.send.bind(res)
288            });
[481]289        };
290    }
291    function makeDocDel_id(type) {
292        return function(req,res) {
[477]293            var id = req.params.id;
[487]294            var doc = req.body;
295            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
296            deleteDocument(id,rev)
297            .handle(res.send.bind(res));
[481]298        };
299    }
300    function makeDocPost(type) {
301        return function(req,res) {
302            var doc = req.body;
[487]303            postDocument(type,doc)
304            .handle({
305                201: function(doc) {
306                    res.set({
307                        'ETag': etags.format([doc._rev])
308                    }).send(201, doc);
309                },
310                default: res.send.bind(res)
311            });
[481]312        };
313    }
314
315    // Questions
[487]316    function getQuestionsWithCode(code) {
317        var url = '_design/questions/_view/by_code';
318        var query = {include_docs:true,key:code};
319        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
320                                              handleUnknownError)
321        .handle({
322            200: handleRowDocs,
323            default: handleUnknownResponse
324        });
325    }
326    function getQuestionsWithTopic(topic) {
327        var url = '_design/questions/_view/all_topics';
328        var query = {reduce:false,include_docs:true,key:topic};
329        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
330                                              handleUnknownError)
331        .handle({
332            200: handleRowDocs,
333            default: handleUnknownResponse
334        });
335    }
336    function getQuestionsWithCategoryAndTopic(category,topic) {
337        var hasTopic = typeof topic !== 'undefined';
338        var url = '_design/questions/_view/all';
339        var query = {reduce:false,include_docs:true,
340                     startkey:hasTopic?[category,topic]:[category],
341                     endkey:hasTopic?[category,topic]:[category,{}]};
342        return HTTPResult.fromResponsePromise(couch.get(url,{query:query}).response,
343                                              handleUnknownError)
344        .handle({
345            200: handleRowDocs,
346            default: handleUnknownResponse
347        });
348    }
[481]349    app.get('/api/questions',
[467]350        ensureAuthenticated,
[487]351        ensureMIME(JSON_MIME),
352        function(req,res) {
353            var hr;
354            if ( 'category' in req.query ) {
355                hr = getQuestionsWithCategoryAndTopic(req.query.category,req.query.topic);
356            } else if ( 'topic' in req.query ) {
357                hr = getQuestionsWithTopic(req.query.topic);
358            } else if ( 'code' in req.query ) {
359                hr = getQuestionsWithCode(req.query.code);
360            }
361            hr.handle(res.send.bind(res));
362        });
363    app.post('/api/questions',
364        ensureAuthenticated,
365        ensureMIME(JSON_MIME),
366        function (req,res) {
367            var doc = req.body;
368            getQuestionsWithCode(doc.code)
369            .handle({
370                200: function(others) {
371                    if ( others.length > 0 ) {
372                        return new HTTPResult(403,{error:"Other question with this code already exists."});
373                    } else {
374                        return postDocument('Question',doc);
375                    }
376                }
377            })
378            .handle(res.send.bind(res));
379        });
[481]380    app.get('/api/questions/:id',
381        ensureAuthenticated,
[487]382        ensureMIME(JSON_MIME),
[481]383        makeDocGet_id('Question'));
384    app.put('/api/questions/:id',
385        ensureAuthenticated,
[487]386        ensureMIME(JSON_MIME),
387        function (req,res) {
388            var id = req.params.id;
389            var doc = req.body;
390            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
391            getQuestionsWithCode(doc.code)
392            .handle({
393                200: function(others) {
[490]394                    if ( others.length > 0 && _.some(others,function(other){ return other._id !== id; })  ) {
[487]395                        return new HTTPResult(403,{error:"Other question with this code already exists."});
396                    } else {
397                        return putDocument(id,rev,'Question',doc);
398                    }
399                }
400            })
401            .handle(res.send.bind(res));
402        });
[481]403    app.delete('/api/questions/:id',
404        ensureAuthenticated,
[487]405        ensureMIME(JSON_MIME),
[481]406        makeDocDel_id('Question'));
[487]407
408
409    // Categories and topics
410    app.get('/api/categories',
[481]411        ensureAuthenticated,
[487]412        ensureMIME(JSON_MIME),
413        function(req,res) {
414            var url = '_design/questions/_view/all';
415            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:1}}).response,
416                                           handleUnknownError)
417            .handle({
418                200: function(result) {
419                    return _.map(result.rows, function(item) {
420                        return { name:item.key[0], count:item.value };
421                    });
422                },
423                default: handleUnknownResponse
424            })
425            .handle(res.send.bind(res));
426        });
427    app.get('/api/categories/:category/topics',
428        ensureAuthenticated,
429        ensureMIME(JSON_MIME),
430        function(req,res) {
431            var category = req.params.category;
432            var url = '_design/questions/_view/all';
433            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true,group_level:2,startkey:[category],endkey:[category,{}]}}).response,
434                                           handleUnknownError)
435            .handle({
436                200: function(result) {
437                    return _.map(result.rows, function(item) { return { name:item.key[1], count:item.value }; });
438                },
439                default: handleUnknownResponse
440            })
441            .handle(res.send.bind(res));
442        });
443    app.get('/api/topics',
444        ensureAuthenticated,
445        ensureMIME(JSON_MIME),
446        function(req,res) {
447            var url = '_design/questions/_view/all_topics';
448            HTTPResult.fromResponsePromise(couch.get(url,{query:{reduce:true,group:true}}).response,
449                                           handleUnknownError)
450            .handle({
451                200: function(result) {
452                    return _.map(result.rows, function(item) { return {name:item.key, count:item.value}; });
453                },
454                default: handleUnknownResponse
455            })
456            .handle(res.send.bind(res));
457        });
[481]458
459    // Surveys
460    app.get('/api/surveys',
461        ensureAuthenticated,
[487]462        ensureMIME(JSON_MIME),
463        function(req,res) {
464            var url;
465            if ( 'drafts' in req.query ) {
466                url = '_design/surveys/_view/drafts';
467            } else if ( 'published' in req.query ) {
468                url = '_design/surveys/_view/published';
469            } else {
470                url = '_design/default/_view/by_type?key='+JSON.stringify('Survey');
471            }
472            HTTPResult.fromResponsePromise(couch.get(url).response,
473                                           handleUnknownError)
474            .handle({
475                200: function(result) {
476                    return _.map(result.rows, function(item) { return item.value; });
477                },
478                default: handleUnknownResponse
479            })
480            .handle(res.send.bind(res));
481        });
482    app.post('/api/surveys',
483        ensureAuthenticated,
484        ensureMIME(JSON_MIME),
485        makeDocPost('Survey'));
[481]486    app.get('/api/surveys/:id',
487        ensureAuthenticated,
[487]488        ensureMIME(JSON_MIME),
[481]489        makeDocGet_id('Survey'));
490    app.put('/api/surveys/:id',
491        ensureAuthenticated,
[487]492        ensureMIME(JSON_MIME),
[481]493        makeDocPut_id('Survey'));
494    app.delete('/api/surveys/:id',
495        ensureAuthenticated,
[487]496        ensureMIME(JSON_MIME),
[481]497        makeDocDel_id('Survey'));
498
499    // SurveyRuns
500    app.get('/api/surveyRuns',
501        ensureAuthenticated,
[487]502        ensureMIME(JSON_MIME),
[481]503        makeDocsGet('SurveyRun'));
[487]504    app.post('/api/surveyRuns',
505        ensureAuthenticated,
506        ensureMIME(JSON_MIME),
507        function(req,res) {
508            var doc = req.body;
509            randomToken()
510            .handle({
511                201: function(token) {
512                    doc.secret = token;
513                    return postDocument('SurveyRun',doc);
514                }
515            })
516            .handle(res.send.bind(res));
517        });
[481]518    app.get('/api/surveyRuns/:id',
519        ensureAuthenticated,
[487]520        ensureMIME(JSON_MIME),
[481]521        makeDocGet_id('SurveyRun'));
[487]522    app.put('/api/surveyRuns/:id',
[481]523        ensureAuthenticated,
[487]524        ensureMIME(JSON_MIME),
525        function (req,res) {
[467]526            var id = req.params.id;
[487]527            var doc = req.body;
528            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
529            var hr;
530            if ( typeof doc.secret === 'undefined' ) {
531                hr = randomToken()
532                .handle({
533                    201: function(token) {
534                        doc.secret = token;
535                        return putDocument(id,rev,'SurveyRun',doc);
536                    }
[475]537                });
[487]538            } else {
539                hr = putDocument(id,rev,'SurveyRun',doc);
540            }
541            hr.handle(res.send.bind(res));
[467]542        });
[487]543    app.delete('/api/surveyRuns/:id',
544        ensureAuthenticated,
545        ensureMIME(JSON_MIME),
546        makeDocDel_id('SurveyRun'));
[481]547    app.get('/api/surveyRuns/:id/responses',
548        ensureAuthenticated,
[487]549        ensureMIME(JSON_MIME),
[481]550        function(req,res) {
551            var id = req.params.id;
[487]552            getResponsesBySurveyRunId(id)
553            .handle(res.send.bind(res));
554        });
555    app.get('/api/surveyRuns/:id/responses.csv',
556        ensureAuthenticated,
557        ensureMIME(CSV_MIME),
558        function(req, res) {
559            var id = req.params.id;
560            getResponsesBySurveyRunId(id)
561            .handle({
562                200: function(responses) {
563                    var flatResponses = responsesToVariables(responses);
564                    res.set({
565                        'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv'
566                    });
567                    res.status(200);
568                    writeObjectsToCSVStream(flatResponses, res);
569                    res.end();
570                },
571                default: res.send.bind(res)
[481]572            });
573        });
[466]574   
[481]575    // Responses
[487]576    function getResponsesBySurveyRunId(surveyRunId) {
577        var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
578        return HTTPResult.fromResponsePromise(couch.get(url).response,
579                                              handleUnknownError)
580        .handle({
581            200: handleRowValues,
582            default: handleUnknownResponse
583        });
584    }
[481]585    app.get('/api/responses',
586        ensureAuthenticated,
[487]587        ensureMIME(JSON_MIME),
588        function(req,res){
589            var hr;
590            if ( 'surveyRunId' in req.query ) {
591                hr = getResponsesBySurveyRunId(req.query.surveyRunId);
592            } else {
593                hr = getDocumentsOfType('Response');
594            }
595            hr.handle(res.send.bind(res));
596        });
[481]597    app.get('/api/responses/:id',
598        ensureAuthenticated,
[487]599        ensureMIME(JSON_MIME),
[481]600        makeDocGet_id('Response'));
601    app.post('/api/responses',
602        ensureAuthenticated,
[487]603        ensureMIME(JSON_MIME),
604        function (req,res) {
605            var doc = req.body;
606            randomToken()
607            .handle({
608                201: function(token) {
609                    doc.secret = token;
610                    return postDocument('Response',doc);
611                }
612            })
613            .handle(res.send.bind(res));
614        });
[481]615    app.put('/api/responses/:id',
616        ensureAuthenticated,
[487]617        ensureMIME(JSON_MIME),
618        function (req,res) {
619            var id = req.params.id;
620            var doc = req.body;
621            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
622            var hr;
623            if ( typeof doc.secret === 'undefined' ) {
624                hr = randomToken()
625                .handle({
626                    201: function(token) {
627                        doc.secret = token;
628                        return putDocument(id,rev,'Response',doc);
629                    }
630                });
631            } else {
632                hr = putDocument(id,rev,'Response',doc);
633            }
634            hr.handle(res.send.bind(res));
635        });
[481]636    app.delete('/api/responses/:id',
637        ensureAuthenticated,
[487]638        ensureMIME(JSON_MIME),
[481]639        makeDocDel_id('Response'));
640
[487]641    //respondents api
642    function isSurveyRunRunning(surveyRun) {
643        var now = new Date();
644        var afterStart = !surveyRun.startDate || now >= new Date(surveyRun.startDate);
645        var beforeEnd = !surveyRun.endDate || now <= new Date(surveyRun.endDate);
646        return afterStart && beforeEnd;
647    }
648    app.get('/api/open/responses/:id',
649        ensureMIME(JSON_MIME),
650        function(req,res) {
651            var id = req.params.id;
652            var rev = etags.parse(req.header('If-Non-Match'))[0];
653            var secret = req.query.secret;
654            getDocument(id,rev,'Response')
655            .handle({
656                200: function(response) {
657                    if ( response.secret === secret ) {
658                        return getDocument(response.surveyRunId,[],'SurveyRun')
659                        .handle({
660                            200: function(surveyRun) {
661                                if ( !isSurveyRunRunning(surveyRun) ) {
662                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
663                                } else {
664                                    response._surveyRun = surveyRun;
665                                    return response;
666                                }
667                            }
668                        });
669                    } else {
670                        return new HTTPResult(403,{error: "Wrong secret for response"});
671                    }
672                }
673            })
674            .handle(res.send.bind(res));
675        });
676    app.post('/api/open/responses',
677        ensureMIME(JSON_MIME),
678        function(req,res) {
679            var secret = req.query.secret;
680            var response = req.body;
681            delete response._surveyRun;
682            getDocument(response.surveyRunId,[],'SurveyRun')
683            .handle({
684                200: function(surveyRun) {
685                    if ( surveyRun.secret !== secret ) {
686                        return new HTTPResult(403,{error:"Wrong secret for surveyRun."});
687                    } else if ( !isSurveyRunRunning(surveyRun) ) {
688                        return new HTTPResult(404,{error:"Survey is not running anymore."});
689                    } else if ( surveyRun.mode === 'closed' ) {
690                        return new HTTPResult(403,{error:"Survey is closed and doesn't allow new responses."});
691                    } else {
692                        return randomToken()
693                        .handle({
694                            201: function(token) {
695                                response.secret = token;
696                                return postDocument('Response',response)
697                                .handle({
698                                    201: function(doc){
699                                        doc._surveyRun = surveyRun;
700                                        return doc;
701                                    }
702                                });
703                            }
704                        });
705                    }
706                }
707            })
708            .handle(res.send.bind(res));
709        });
710    app.put('/api/open/responses/:id',
711        ensureMIME(JSON_MIME),
712        function(req,res){
713            var id = req.params.id;
714            var doc = req.body;
715            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
716            var secret = req.query.secret || doc.secret;
717            delete doc._surveyRun;
718            getDocument(id,[],'Response')
719            .handle({
720                200: function(prev) {
721                    if ( prev.secret !== secret ) {
722                        return new HTTPResult(403,{error: "Secrets are not the same."});
723                    } else if ( prev.surveyRunId !== doc.surveyRunId ) {
724                        return new HTTPResult(400,{error:"Response cannot change it's surveyRunId."});
725                    } else {
726                        doc.secret = secret; // restore in case it got lost or was changed
727                        return getDocument(doc.surveyRunId,[],'SurveyRun')
728                        .handle({
729                            200: function(surveyRun) {
730                                if ( !isSurveyRunRunning(surveyRun) ) {
731                                    return new HTTPResult(404,{error:"Survey is not running anymore."});
732                                } else {
733                                    return putDocument(id,rev,'Response',doc)
734                                    .handle({
735                                        200: function(doc) {
736                                            doc._surveyRun = surveyRun;
737                                            return new HTTPResult(201,doc);
738                                        }
739                                    });
740                                }
741                            }
742                        });
743                    }
744                }
745            })
746            .handle(res.send.bind(res));
747        });
748    app.delete('/api/open/responses/:id',
749        ensureMIME(JSON_MIME),
750        function(req,res){
751            var id = req.params.id;
752            var doc = req.body;
753            var rev = etags.parse(req.header('If-Match'))[0] || (doc && doc._rev);
754            var secret = req.query.secret || doc.secret;
755            delete doc._surveyRun;
756            getDocument(id,[],'Response')
757            .handle({
758                200: function(prev) {
759                    if ( prev.secret !== secret ) {
760                        return new HTTPResult(403,{error: "Secrets are not the same."});
761                    } else {
762                        return deleteDocument(id,rev,doc);
763                    }
764                }
765            })
766            .handle(res.send.bind(res));
767        });
768
769    // uuids
770    app.get('/api/uuids',
771        ensureAuthenticated,
772        ensureMIME(JSON_MIME),
773        function(req,res){
774            var count = (req.query.count && parseInt(req.query.count,10)) || 1;
775            HTTPResult.fromResponsePromise(couch.uuids({query:{count:count}}).response,
776                                           handleUnknownError)
777            .handle({
778                200: function(res) {
779                    return res.uuids;
780                },
781                default: handleUnknownResponse
782            })
783            .handle(res.send.bind(res));
784        });
785
[463]786    return app;
[481]787}
[475]788
789function responsesToVariables(responses) {
790    return _.map(responses, responseToVariables);
791}
792
793function responseToVariables(response) {
794    var result = flattenObject(response.answers);
795    return result;
796}
797
798function flattenObject(value) {
799    var result = {};
800    (function agg(val,key,res){
801        if ( _.isObject(val) ) {
[477]802            var keys = _.keys(val);
803            // FIXME : dirty hack for questions with only one input
804            if ( keys.length === 1 ) {
805                agg(val[keys[0]],key,res);
806            } else {
807                _.forEach(val, function(v,k){
808                    agg(v,(key ? key+'.' : '')+k,res);
809                });
810            }
[475]811        } else if ( _.isArray(val) ) {
[477]812            // FIXME : dirty hack for questions with only one input
813            if ( val.length === 1 ) {
814                agg(val[0],key,res);
815            } else {
816                _.forEach(val, function(v,i){
817                    agg(v,(key ? key+'.' : '')+i,res);
818                });
819            }
[475]820        } else {
821            res[key] = val;
822        }
823    })(value,null,result);
824    return result;
825}
826
[478]827function writeObjectsToCSVStream(objects, stream, prelude) {
[475]828    var keys = _.chain(objects)
829                .map(_.keys)
830                .flatten()
831                .uniq()
832                .value();
833    var idxs = {};
834    _.forEach(keys, function(key,idx){
835        idxs[key] = idx;
836    });
837    var writer = new CSV.CsvWriter(stream);
[478]838    if ( prelude ) {
839        _.forEach(prelude, function(val,key){
840            writer.writeRecord([key,val]);
841        });
842    }
[475]843    writer.writeRecord(keys);
844    _.forEach(objects, function(obj){
845        var row = [];
846        _.forEach(obj, function(val,key){
847            row[idxs[key]] = val;
848        });
849        writer.writeRecord(row);
850    });
851}
[487]852
853function randomToken() {
854    var result = new HTTPResult();
855    cryptoken(8)
856    .then(function(token){
857        result.set(201,token);
858    }, function(ex){
859        result.set(500,{error:"Cannot generate secrets.", innerError: ex});
860    });
861    return result;
862}
Note: See TracBrowser for help on using the repository browser.