Coverage

100%
423
423
0

aestimia.js

100%
6
6
0
LineHitsSource
11exports.app = require('./app');
21exports.models = require('./models');
31exports.api = require('./api');
41exports.website = require('./website');
51exports.filters = require('./filters');
61exports.documentation = require('./documentation');

api.js

100%
43
43
0
LineHitsSource
11var models = require('./models');
2
31function handleMongooseError(err, res, next) {
43 if (err.name == "ValidationError") {
51 res.send(422, makeConciseValidationError(err));
62 } else if (err.name == "CastError") {
71 res.send(422, {message: err.message});
8 } else {
91 next(err);
10 }
11}
12
131function makeConciseValidationError(err) {
141 var errors = {};
151 Object.keys(err.errors).forEach(function(error) {
161 errors[error] = err.errors[error].message;
17 });
181 return {
19 message: "Validation Error",
20 errors: errors
21 };
22}
23
241exports.getSubmission = function(req, res, next) {
251 res.send(res.locals.submission);
26};
27
281exports.listSubmissions = function(req, res, next) {
292 if (!req.query.learner)
301 return res.send(422, {"message": "invalid search query"});
31
321 models.Submission.find({
33 learner: req.query.learner
34 }, function(err, submissions) {
351 if (err) return next(err);
361 return res.send(submissions);
37 });
38};
39
401exports.listMentors = function(req, res, next) {
411 models.Mentor.find(function(err, mentors) {
421 if (err) return next(err);
43
441 var json = mentors.map(function(mentor) {
451 return {
46 email: mentor.email,
47 classifications: mentor.classifications
48 };
49 });
50
511 res.send(json);
52 });
53};
54
551exports.changeMentor = function(req, res, next) {
563 var email = req.body.email;
573 var classifications = req.body.classifications;
58
593 if (typeof(email) != "string")
601 return res.send(422, {message: "invalid email"});
61
622 if (!classifications || !classifications.length)
631 models.Mentor.remove({email: email}, function(err) {
641 if (err) return handleMongooseError(err, res, next);
651 res.send(200, {message: "deleted"});
66 });
67 else
681 models.Mentor.update({email: email}, {
69 email: email,
70 classifications: classifications
71 }, {
72 upsert: true
73 }, function(err) {
741 if (err) return handleMongooseError(err, res, next);
751 res.send(200, {message: "updated"});
76 });
77};
78
791exports.submit = function(req, res, next) {
804 var submission = new models.Submission(req.body);
814 submission.save(function(err, submission) {
827 if (err) return handleMongooseError(err, res, next);
831 return res.send(201, {
84 id: submission._id.toString()
85 });
86 });
87};

app.js

100%
100
100
0
LineHitsSource
11var path = require('path');
21var url = require('url');
31var fs = require('fs');
41var _ = require('underscore');
51var express = require('express');
61var nunjucks = require('nunjucks');
71var flash = require('connect-flash');
81var clientSessions = require('client-sessions');
9
101var api = require('./api');
111var website = require('./website');
121var filters = require('./filters');
131var paths = require('./paths');
14
151const PERSONA_JS_URL = "https://login.persona.org/include.js";
16
171function securityHeaders(options) {
188 return function(req, res, next) {
1960 res.set('X-Frame-Options', 'DENY');
2060 res.set('X-Content-Type-Options', 'nosniff');
2160 if (options.enableHSTS)
221 res.set('Strict-Transport-Security',
23 'max-age=31536000; includeSubDomains');
24
2560 addContentSecurityPolicy(req, res);
2660 next();
27 };
28}
29
301function addContentSecurityPolicy(req, res) {
3160 var policies = {
32 'default-src': ["'self'"],
33 'script-src': [
34 "'self'",
35 "https://login.persona.org"
36 ],
37 'frame-src': ['https://login.persona.org'],
38 'style-src': ["'self'", "'unsafe-inline'"],
39 'img-src': ['*'],
40 // options is deprecated, but Firefox still needs it.
41 'options': []
42 };
4360 if (req.path == '/test/') {
44 // Some of our testing tools, e.g. sinon, use eval(), so we'll
45 // enable it for this one endpoint.
464 policies['script-src'].push("'unsafe-eval'");
474 policies['options'].push('eval-script');
48 }
4960 var directives = [];
5060 Object.keys(policies).forEach(function(directive) {
51360 directives.push(directive + ' ' + policies[directive].join(' '));
52 });
5360 var policy = directives.join('; ');
5460 res.set('Content-Security-Policy', policy);
5560 res.set('X-Content-Security-Policy', policy);
5660 res.set('X-WebKit-CSP', policy);
57}
58
591function applyTheme(themeDir, app, loaders) {
601 themeDir = path.resolve(__dirname, "..", themeDir);
61
621 var appLocalsFile = path.join(themeDir, 'app-locals.json');
631 var viewsDir = path.join(themeDir, 'views');
641 var staticDir = path.join(themeDir, 'static');
65
661 loaders.push(new nunjucks.FileSystemLoader(viewsDir));
671 app.use('/theme/', express.static(staticDir));
68
691 if (fs.existsSync(appLocalsFile))
701 _.extend(app.locals, JSON.parse(fs.readFileSync(appLocalsFile, 'utf8')));
71
721 app.locals.THEME_ROOT = '/theme/';
731};
74
751exports.build = function(options) {
768 var app = express();
77
788 app.configure(function() {
798 var env;
808 var loaders = [];
818 var personaJsUrl = options.personaIncludeJs || PERSONA_JS_URL;
828 var definePersonaRoutes = options.personaDefineRoutes ||
83 require('express-persona');
848 var csrf = express.csrf();
858 var apiAuth = options.apiKey
86 ? express.basicAuth('api', options.apiKey)
87 : function(req, res, next) {
881 res.send(403, 'API access is disabled.');
89 };
90
918 app.use(securityHeaders({
92 enableHSTS: url.parse(options.personaAudience).protocol == "https:"
93 }));
948 app.use(express.static(paths.staticDir));
958 if (options.debug)
962 app.use('/test', express.static(paths.staticTestDir));
97
988 _.extend(app.locals, {
99 DOT_MIN: options.debug ? '' : '.min',
100 PERSONA_JS_URL: personaJsUrl,
101 APP_NAME: 'Aestimia'
102 });
103
1048 if (options.themeDir)
1051 applyTheme(options.themeDir, app, loaders);
106
1078 loaders.push(new nunjucks.FileSystemLoader(paths.viewsDir));
1088 env = new nunjucks.Environment(loaders, {
109 autoescape: true
110 });
111
1128 env.express(app);
1138 Object.keys(filters).forEach(function(name) {
1148 env.addFilter(name, filters[name]);
115 });
1168 app.nunjucksEnv = env;
117
1188 app.use(function(req, res, next) {
11949 res.header("Cache-Control", "no-cache");
12049 next();
121 });
1228 app.use(express.bodyParser());
1238 app.use(clientSessions({
124 cookieName: 'session',
125 secret: options.cookieSecret,
126 duration: 24 * 60 * 60 * 1000, // defaults to 1 day
127 }));
128
1299 if (options.defineExtraMiddleware) options.defineExtraMiddleware(app);
130
1318 app.use(function CsrfOrApiAuth(req, res, next) {
13249 if (req.path.match(/^\/api\//)) {
13313 req.isApi = true;
13413 return apiAuth(req, res, next);
135 } else
13636 return csrf(req, res, next);
137 });
1388 app.use(flash());
1398 app.use(website.setResponseLocalsForTemplates);
140
1418 definePersonaRoutes(app, {audience: options.personaAudience});
142 });
143
1448 app.param('submissionId', website.findSubmissionById);
145
1468 app.get('/', website.index);
1478 app.get('/history', website.history);
1488 app.get('/submissions/:submissionId', website.submissionDetail);
1498 app.post('/submissions/:submissionId', website.submitAssessment);
1508 app.get('/demo', website.demo);
1518 app.get('/docs', website.docs);
152
1538 app.get('/api/submissions/:submissionId', api.getSubmission);
1548 app.get('/api/submissions', api.listSubmissions);
1558 app.post('/api/submission', api.submit);
1568 app.get('/api/mentors', api.listMentors);
1578 app.post('/api/mentor', api.changeMentor);
158
15912 if (options.defineExtraRoutes) options.defineExtraRoutes(app);
160
1618 app.use(function(err, req, res, next) {
1622 if (typeof(err.status) == 'number')
1631 return res.type('text/plain').send(err.status, err.message);
1641 process.stderr.write(err.stack);
1651 res.send(500, 'Sorry, something exploded!');
166 });
167
1688 return app;
169};

documentation.js

100%
33
33
0
LineHitsSource
11var fs = require('fs');
21var path = require('path');
31var cheerio = require('cheerio');
41var nunjucks = require('nunjucks');
5
61var data = require('../test/data');
71var paths = require('./paths');
8
9// This is synchronous, which is bad, but it's only called from
10// the API documentation pages, which will only get hit by
11// developers.
121exports.parseRawApiSections = function parseRawApiSections() {
134 var docs = fs.readFileSync(paths.fromRoot('doc', 'api-raw.html'),
14 'utf8');
154 var $ = cheerio.load(docs);
164 var sections = [];
17
184 $("section").each(function() {
1928 var title = $(this).find("h2").remove();
2028 var template = new nunjucks.Template($(this).html());
21
2228 sections.push({
23 title: title.text(),
24 id: $(this).attr('id'),
25 html: template.render({examples: data.docExamples})
26 });
27 });
28
294 return sections;
30};
31
321function copyAndRewrite(query, srcAttr, outdir) {
335 var src = query.attr(srcAttr);
345 var fullPath = path.join(paths.staticDir, src);
355 var filename = src.split('/').slice(-1)[0];
365 var content = fs.readFileSync(fullPath);
37
385 fs.writeFileSync(path.join(outdir, filename), content);
395 query.attr(srcAttr, filename);
40}
41
421exports.generateStaticDocs = function(outdir, commitHash) {
431 var loaders = [
44 new nunjucks.FileSystemLoader(paths.fromRoot('doc')),
45 new nunjucks.FileSystemLoader(paths.viewsDir)
46 ];
471 var env = new nunjucks.Environment(loaders, {autoescape: true});
481 var content = env.render('docs.html', {
49 sections: exports.parseRawApiSections(),
50 generatingStaticDocs: true,
51 commitHash: commitHash
52 });
531 var $ = cheerio.load(content);
54
552 if (!fs.existsSync(outdir)) fs.mkdirSync(outdir);
56
571 $('script').each(function() {
583 copyAndRewrite($(this), "src", outdir);
59 });
601 $('link[rel="stylesheet"]').each(function() {
612 copyAndRewrite($(this), "href", outdir);
62 });
631 fs.writeFileSync(path.join(outdir, "index.html"), $.html());
64};

filters.js

100%
5
5
0
LineHitsSource
11var _ = require('underscore');
21var SafeString = require('nunjucks/src/runtime').SafeString;
3
41exports.timeago = function(date) {
524 var html = '<time class="timeago" datetime="' +
6 _.escape(date.toISOString()) +
7 '">' + _.escape(date.toUTCString()) + '</time>';
824 return new SafeString(html);
9};

models/index.js

100%
4
4
0
LineHitsSource
11exports.Mentor = require('./mentor');
21exports.Submission = require('./submission');
31exports.paginate = require('./paginate');
41exports.validators = require('./validators');

models/mentor.js

100%
14
14
0
LineHitsSource
11var _ = require('underscore');
21var mongoose = require('mongoose');
3
41var validEmail = require('./validators').validEmail;
5
61var mentorSchema = new mongoose.Schema({
7 email: {type: String, required: true, unique: true, validate: validEmail},
8 classifications: [String]
9});
10
111var Mentor = mongoose.model('Mentor', mentorSchema);
12
131Mentor.classificationsFor = function classificationsFor(email, cb) {
1484 Mentor.findOne({
15 email: email
16 }, function(err, mentor) {
1784 if (err) return cb(err);
1884 var userClassifications = mentor ? mentor.classifications : [];
1984 Mentor.findOne({
20 email: '*@' + email.split('@')[1]
21 }, function(err, mentor) {
2284 if (err) return cb(err);
2384 var domainClassifications = mentor ? mentor.classifications : [];
2484 cb(null, _.union(userClassifications, domainClassifications));
25 });
26 });
27};
28
291module.exports = Mentor;

models/paginate.js

100%
19
19
0
LineHitsSource
11var assert = require('assert');
2
31var _ = require('underscore');
41var async = require('async');
5
61var Model = require('mongoose').Model;
7
81Model.paginate = function(options, cb) {
911 options.page = parseInt(options.page);
1011 options.resultsPerPage = parseInt(options.resultsPerPage);
11
1211 assert(options.page > 0, "page must be positive");
1311 assert(options.resultsPerPage > 0, "resultsPerPage must be positive");
14
1511 var model = this;
1611 var criteria = options.criteria;
1711 var resultsPerPage = options.resultsPerPage;
1811 var queryOptions = _.extend({}, options.options, {
19 skip: (options.page-1) * resultsPerPage,
20 limit: resultsPerPage
21 });
22
2311 async.waterfall([
24 function(done) {
2511 model.find(criteria).setOptions(queryOptions).exec(done)
26 },
27 function(results, done) {
2811 model.count(criteria).exec(function(err, count) {
2911 var totalPages = Math.ceil(count / resultsPerPage);
3011 done(err, results, totalPages);
31 });
32 }
33 ], cb);
34};
35
361module.exports = Model.paginate;

models/submission.js

100%
77
77
0
LineHitsSource
11var _ = require('underscore');
21var async = require('async');
31var mongoose = require('mongoose');
4
51var validators = require('./validators');
61var validEmail = validators.validEmail;
71var safeUrl = validators.safeUrl;
81var validMediaType = validators.validMediaType;
91var Mentor = require('./mentor');
10
111var reviewSchema = new mongoose.Schema({
12 date: {type: Date, default: Date.now},
13 author: {type: String, required: true, validate: validEmail},
14 response: {type: String, required: true},
15 satisfiedRubrics: [Number]
16});
17
181var submissionSchema = new mongoose.Schema({
19 learner: {type: String, required: true, validate: validEmail},
20 criteriaUrl: {type: String, required: true, validate: safeUrl},
21 achievement: {
22 name: String,
23 description: String,
24 imageUrl: {type: String, validate: safeUrl}
25 },
26 cannedResponses: [String],
27 // TODO: Ensure that at least one non-empty classification exists.
28 classifications: [String],
29 evidence: [
30 {
31 url: {type: String, validate: safeUrl},
32 mediaType: {type: String, default: "link", validate: validMediaType},
33 reflection: String
34 }
35 ],
36 flagged: {type: Boolean, default: false},
37 onChangeUrl: {type: String, validate: safeUrl},
38 creationDate: {type: Date, default: Date.now},
39 assignedTo: {
40 mentor: {type: String, validate: validEmail},
41 expiry: Date
42 },
43 // TODO: Ensure that at least one rubric is required.
44 rubric: {
45 items: [
46 {
47 required: Boolean,
48 text: String
49 }
50 ]
51 },
52 reviews: [reviewSchema],
53});
54
551reviewSchema.pre('save', function(next) {
5665 var parent = this.ownerDocument();
5765 var err;
58
5965 if (parent.cannedResponses.length) {
602 if (parent.cannedResponses.indexOf(this.response) == -1) {
611 err = new Error("review response is not in list of canned responses");
621 err.name = "ValidationError";
631 err.errors = {response: {message: err.message}};
641 return next(err);
65 }
66 }
6764 next(null);
68});
69
701reviewSchema.pre('save', function(next) {
7164 var parent = this.ownerDocument();
7264 var self = this;
73
7464 Mentor.classificationsFor(self.author, function(err, classifications) {
7564 if (err)
761 return next(err);
7763 if (_.intersection(classifications, parent.classifications).length)
7862 return next();
791 err = new Error("reviewer " + self.author + " does not have " +
80 "permission to review");
811 err.name = "ValidationError";
821 err.errors = {reviewer: {message: err.message}};
831 next(err);
84 });
85});
86
87// Atomically assign the given mentor w/ the given expiry time to the
88// submission.
89//
90// Calls cb with (err, submission). If the submission is already assigned
91// to someone, submission will be null.
92//
93// The document this method is called on becomes invalidated immediately
94// after use; if you need to keep doing things with it, use the submission
95// object passed to the callback.
961submissionSchema.methods.assignTo = function(email, expiry, cb) {
978 var criteria = {_id: this._id};
988 if (this.assignedTo.mentor != email &&
99 this.assignedTo.expiry &&
100 this.assignedTo.expiry.getTime() > Date.now())
1012 return cb(null, null);
1026 if (this.assignedTo.mentor)
1032 criteria.assignedTo = {
104 mentor: this.assignedTo.mentor,
105 expiry: this.assignedTo.expiry
106 };
1076 Submission.findOneAndUpdate(criteria, {
108 assignedTo: {
109 mentor: email,
110 expiry: expiry
111 }
112 }, function(err, submission) {
1136 if (err) return cb(err);
1146 cb(null, submission);
115 });
116};
117
1181submissionSchema.methods.getAssignee = function() {
11919 if (!this.assignedTo.expiry ||
120 this.assignedTo.expiry.getTime() <= Date.now())
12110 return null;
122
1239 return this.assignedTo.mentor;
124};
125
1261submissionSchema.methods.latestReview = function() {
1278 return this.reviews[this.reviews.length-1];
128};
129
1301submissionSchema.methods.isReviewed = function() {
13145 return !!this.reviews.length || this.flagged;
132};
133
1341submissionSchema.methods.isAwarded = function() {
1358 if (!this.reviews.length)
1361 return false;
1377 var satisfied = this.latestReview().satisfiedRubrics;
1387 var result = true;
1397 this.rubric.items.forEach(function(item, i) {
14028 if (item.required && satisfied.indexOf(i) == -1)
1414 result = false;
142 }, this);
1437 return result;
144};
145
1461submissionSchema.methods.canBeReviewedBy = function(email, cb) {
14711 var self = this;
148
14911 async.waterfall([
150 Mentor.classificationsFor.bind(Mentor, email),
151 function(classifications, done) {
15211 if (_.intersection(classifications, self.classifications).length)
15310 return done(null, true);
1541 return done(null, false);
155 }
156 ], cb);
157};
158
1591submissionSchema.methods.isLearnerUnderage = function() {
16023 return !!this.cannedResponses.length;
161};
162
1631var Submission = mongoose.model('Submission', submissionSchema);
164
1651function findSubmissions(options, cb) {
1667 async.waterfall([
167 Mentor.classificationsFor.bind(Mentor, options.email),
168 function(classifications, done) {
1697 options.criteria.classifications = {$in: classifications};
1707 Submission.paginate({
171 criteria: options.criteria,
172 options: options.options,
173 page: options.page,
174 resultsPerPage: options.resultsPerPage
175 }, done);
176 }
177 ], cb);
178}
179
1801Submission.findReviewed = function(options, cb) {
1812 findSubmissions({
182 criteria: {
183 $or: [
184 {reviews: {$not: {$size: 0}}},
185 {flagged: true}
186 ]
187 },
188 options: {sort: '-creationDate'},
189 email: options.email,
190 page: options.page,
191 resultsPerPage: options.resultsPerPage
192 }, cb);
193};
194
1951Submission.findForReview = function(options, cb) {
1965 findSubmissions({
197 criteria: {
198 $and: [
199 {reviews: {$size: 0}},
200 {flagged: false}
201 ]
202 },
203 options: {sort: 'creationDate'},
204 email: options.email,
205 page: options.page,
206 resultsPerPage: options.resultsPerPage
207 }, cb);
208};
209
2101module.exports = Submission;
211
212// This is only here so tests can mock/test things out.
2131Submission.Mentor = Mentor;

models/validators.js

100%
5
5
0
LineHitsSource
11exports.validEmail = [
2 function(email) {
3 // Apparently using a regexp to do anything more strict than this
4 // is hard: http://stackoverflow.com/a/201378
5393 return /^([^@]+)@([^@]+)$/.test(email);
6 },
7 "email must be of form user@host"
8];
9
101049exports.safeUrl = [function(url) { return /^https?:\/\//.test(url); },
11 "url must be http or https"];
12
131exports.validMediaType = [
14 function(value) {
15701 return ['image', 'link'].indexOf(value) != -1;
16 },
17 'invalid media type'
18];

paths.js

100%
7
7
0
LineHitsSource
11var path = require('path');
2
31var fromRoot = exports.fromRoot = function fromRoot() {
48 var fullPath = path.join.apply(this, arguments);
58 return path.resolve(__dirname + '/..', fullPath);
6};
7
81exports.viewsDir = fromRoot('views');
91exports.staticDir = fromRoot('static');
101exports.staticTestDir = fromRoot('test', 'browser');

website.js

100%
110
110
0
LineHitsSource
11var request = require('request');
2
31var Submission = require('./models').Submission;
41var demoData = require('../test/data');
51var docs = require('./documentation');
6
71const RESULTS_PER_PAGE = 10;
81const ASSIGNMENT_LOCKOUT_MS = exports.ASSIGNMENT_LOCKOUT_MS = 1000 * 60 * 15;
9
101function callWebhook(submission) {
115 if (submission.onChangeUrl) {
122 request.post({
13 url: submission.onChangeUrl,
14 json: {
15 _id: submission._id
16 }
17 }, function(err) {
182 if (err)
191 console.error("calling webhook", submission.onChangeUrl,
20 "for submission", submission._id.toString(),
21 "failed with error", err.message);
22 });
23 }
24}
25
261exports.findSubmissionById = function(req, res, next, id) {
2716 Submission.findOne({_id: id}, function(err, submission) {
2816 function proceed() {
2911 res.locals.submission = submission;
3011 next();
31 }
32
3316 if (err) {
342 if (err.name == "CastError")
351 return res.send(404);
361 return next(err);
37 }
3814 if (!submission)
391 return res.send(404);
4013 if (req.isApi)
411 return proceed();
4212 if (!req.session.email)
431 return res.status(401).render('access-denied.html');
4411 submission.canBeReviewedBy(req.session.email, function(err, result) {
4511 if (err) return next(err);
4611 if (result)
4710 return proceed();
481 return res.status(403).render('access-denied.html');
49 });
50 });
51};
52
531exports.submissionDetail = function(req, res, next) {
541 res.render('submission-detail.html');
55};
56
571function assignAssessment(req, res, next) {
582 var exp = Date.now() + ASSIGNMENT_LOCKOUT_MS;
592 res.locals.submission.assignTo(req.session.email, exp, function(err, sub) {
602 if (err) return next(err);
612 if (sub) {
621 return res.redirect(303, req.path + '#assess');
63 } else {
641 req.flash('error', 'Sorry, someone else is already assessing this.');
651 return res.redirect(303, req.path);
66 }
67 });
68}
69
701function unassignAssessment(req, res, next) {
711 var exp = Date.now();
721 res.locals.submission.assignTo(req.session.email, exp, function(err, sub) {
731 if (err) return next(err);
741 return res.redirect(303, req.path);
75 });
76}
77
781exports.submitAssessment = function(req, res, next) {
799 function respond(flashType, flashMsg) {
805 return function(err) {
815 if (err) return next(err);
825 callWebhook(submission);
835 req.flash(flashType, flashMsg);
845 return res.redirect(303, req.path);
85 };
86 }
87
889 var submission = res.locals.submission;
899 var satisfiedRubrics = [];
90
919 if (req.body['action'] == 'assign')
922 return assignAssessment(req, res, next);
93
947 if (req.body['action'] == 'unassign')
951 return unassignAssessment(req, res, next);
96
976 if (req.body['action'] == 'flag') {
981 return Submission.update({
99 _id: submission._id
100 }, {
101 flagged: true
102 }, respond('info', 'Assessment reported for inappropriate content.'));
103 }
104
1055 if (req.body['action'] == 'unflag') {
1061 return Submission.update({
107 _id: submission._id
108 }, {
109 flagged: false
110 }, respond('info', 'Assessment un-reported for inappropriate content.'));
111 }
112
1134 req.body.response = req.body.response && req.body.response.trim();
114
1154 if (!req.body.response) {
1161 req.flash('error', 'Please provide an assessment response.');
1171 return res.redirect(303, req.path);
118 }
119
1203 submission.rubric.items.forEach(function(item, i) {
12112 if (req.body['rubric_' + i] == 'on')
1222 satisfiedRubrics.push(i);
123 });
1243 Submission.update({
125 _id: submission._id
126 }, {
127 $push: {
128 reviews: {
129 author: req.session.email,
130 response: req.body.response,
131 satisfiedRubrics: satisfiedRubrics
132 }
133 }
134 }, respond('success', 'Assessment submitted.'));
135};
136
1371exports.demo = function(req, res, next) {
1381 var submissions = [];
1391 Object.keys(demoData.submissions).forEach(function(name) {
1402 submissions.push({
141 name: name,
142 json: JSON.stringify(demoData.submissions[name], null, 2)
143 });
144 });
1451 res.render('demo.html', {
146 submissions: submissions,
147 sections: docs.parseRawApiSections()
148 });
149};
150
1511exports.docs = function(req, res) {
1521 return res.render('docs.html', {sections: docs.parseRawApiSections()});
153};
154
1551var showPaginatedSubmissions = exports.showPaginatedSubmissions =
156function showPaginatedSubmissions(methodName, view, req, res, next) {
15717 var page = parseInt(req.query.page);
15817 var linkToPage = function(page) {
1598 return req.path + '?page=' + page;
160 };
161
16231 if (isNaN(page) || page <= 0) page = 1;
163
16417 Submission[methodName]({
165 email: req.session.email,
166 page: page,
167 resultsPerPage: RESULTS_PER_PAGE
168 }, function(err, submissions, totalPages) {
16914 if (err) return next(err);
17012 if (!submissions.length && totalPages)
1711 return res.redirect(302, linkToPage(totalPages));
17211 res.render(view, {
173 submissions: submissions,
174 page: page,
175 totalPages: totalPages,
176 prevPage: (page > 1) && linkToPage(page - 1),
177 nextPage: (page < totalPages) && linkToPage(page + 1)
178 });
179 });
180}
181
1821exports.history = function(req, res, next) {
1832 if (req.session.email) {
1841 showPaginatedSubmissions('findReviewed', 'history.html', req, res, next);
185 } else
1861 res.status(401).render('access-denied.html');
187};
188
1891exports.index = function(req, res, next) {
19011 if (req.session.email) {
1914 showPaginatedSubmissions('findForReview', 'queue.html', req, res, next);
192 } else
1937 res.render('splash.html');
194};
195
1961function makeGetFlashMessages(req) {
19746 var cached = null;
198
19946 return function() {
20020 if (!cached) {
20119 cached = [];
20219 ['error', 'success', 'info'].forEach(function(category) {
20357 req.flash(category).forEach(function(html) {
2041 cached.push({
205 category: category,
206 html: html
207 });
208 });
209 });
210 }
21120 return cached;
212 };
213}
214
2151exports.setResponseLocalsForTemplates = function(req, res, next) {
21646 res.locals.csrfToken = req.session._csrf;
21746 res.locals.email = req.session.email;
21846 res.locals.messages = makeGetFlashMessages(req);
21946 next();
220};