From 74eda889be2733a32ab7c0e0c2ec3f306793cc90 Mon Sep 17 00:00:00 2001 From: Thomas FORGIONE Date: Fri, 22 May 2015 12:04:39 +0200 Subject: [PATCH] Monster commit : a lot of cleaning - Removed the series of nested callback in controller/prototype/index.js. Now, there are dbrequests that gives functions to easily access results of SQL requests. - Cleaning in the views : removed if / else if / else if in prototype and replaced by template inheritance --- controllers/prototype/dbrequests.js | 345 ++++++++++++++++++ controllers/prototype/index.js | 300 ++------------- controllers/prototype/views/prototype.jade | 86 +---- .../prototype/views/prototype_arrows.jade | 14 + .../views/prototype_interactive.jade | 51 +++ .../prototype/views/prototype_replays.jade | 12 + .../prototype/views/prototype_reverse.jade | 17 + .../prototype/views/prototype_viewports.jade | 16 + controllers/prototype/views/replay_index.jade | 4 +- 9 files changed, 495 insertions(+), 350 deletions(-) create mode 100644 controllers/prototype/dbrequests.js create mode 100644 controllers/prototype/views/prototype_arrows.jade create mode 100644 controllers/prototype/views/prototype_interactive.jade create mode 100644 controllers/prototype/views/prototype_replays.jade create mode 100644 controllers/prototype/views/prototype_reverse.jade create mode 100644 controllers/prototype/views/prototype_viewports.jade diff --git a/controllers/prototype/dbrequests.js b/controllers/prototype/dbrequests.js new file mode 100644 index 0000000..5118290 --- /dev/null +++ b/controllers/prototype/dbrequests.js @@ -0,0 +1,345 @@ +var pg = require('pg'); +var pgc = require('../../private.js'); + +var Info = function(id, finishAction) { + this.id = id; + + this.ready = { + cameras: false, + coins: false, + arrows: false, + resets : false, + previousNext: false, + hovered: false + }; + + this.results = {}; + this.finishAction = finishAction; + + // Connect to db + var self = this; + pg.connect(pgc.url, function(err, client, release) { + self.client = client; + self.release = release; + self.loadAll(); + }); +} + +Info.prototype.loadAll = function() { + this.loadCameras(); + this.loadCoins(); + this.loadArrows(); + this.loadResets(); + this.loadPreviousNext(); + this.loadHovered(); +} + +Info.prototype.tryMerge = function() { + // If not ready, do nothing + for (var i in this.ready) { + if (!this.ready[i]) { + return; + } + } + + // Release db connection + this.release(); + this.release = null; + this.client = null; + + this.merge(); + this.finishAction(this.finalResult); +} + +Info.prototype.merge = function() { + this.finalResult = []; + + for (;;) { + // Find next element + var nextElement = null; + var nextIndex = null; + + for (var i in this.results) { + // The next element is placed at the index 0 (since the elements + // gotten from the database are sorted) + if (this.results[i].length !== 0 && + (nextElement === null || this.results[i][0].time < nextElement.time)) { + nextElement = this.results[i][0]; + nextIndex = i; + } + } + + // If there is no next element, we're done + if (nextElement === null) { + break; + } + + // Add the next element in results and shift its table + this.finalResult.push(this.results[nextIndex].shift()); + } +} + +Info.prototype.loadCameras = function() { + var self = this; + this.client.query( + "SELECT ((camera).position).x AS px, " + + "((camera).position).y AS py, " + + "((camera).position).z AS pz, " + + "((camera).target).x AS tx, " + + "((camera).target).y AS ty, " + + "((camera).target).z AS tz, " + + "time AS time " + + "FROM keyboardevent WHERE user_id = $1 ORDER BY time;", + [self.id], + function(err, result) { + self.results.cameras = []; + for (var i in result.rows) { + self.results.cameras.push( + { + type: 'camera', + position : { + x: result.rows[i].px, + y: result.rows[i].py, + z: result.rows[i].pz + }, + target : { + x: result.rows[i].tx, + y: result.rows[i].ty, + z: result.rows[i].tz + }, + time: result.rows[i].time + } + ); + } + self.ready.cameras = true; + self.tryMerge(); + } + ); +} + +Info.prototype.loadCoins = function() { + var self = this; + this.client.query( + "SELECT coin_id, time FROM coinclicked WHERE user_id = $1 ORDER BY time;", + [self.id], + function(err,result) { + self.results.coins = []; + for (var i in result.rows) { + self.results.coins.push( + { + type: 'coin', + time: result.rows[i].time, + id: result.rows[i].coin_id + } + ); + } + self.ready.coins = true; + self.tryMerge(); + } + ); +} + +Info.prototype.loadArrows = function() { + var self = this; + this.client.query( + "SELECT arrow_id, time FROM arrowclicked WHERE user_id = $1 ORDER BY time;", + [self.id], + function(err, result) { + self.results.arrows = []; + for (var i in result.rows) { + self.results.arrows.push( + { + type: 'arrow', + time: result.rows[i].time, + id: result.rows[i].arrow_id + } + ); + } + self.ready.arrows = true; + self.tryMerge(); + } + ); +} + +Info.prototype.loadResets = function() { + var self = this; + this.client.query( + "SELECT time FROM resetclicked WHERE user_id = $1 ORDER BY time;", + [self.id], + function(err, result) { + self.results.resets = []; + for (var i in result.rows) { + self.results.resets.push( + { + type: 'reset', + time: result.rows[i].time + } + ); + } + self.ready.resets = true; + self.tryMerge(); + } + ); +} + +Info.prototype.loadPreviousNext = function () { + var self = this; + this.client.query( + "SELECT ((camera).position).x AS px, " + + "((camera).position).y AS py, " + + "((camera).position).z AS pz, " + + "((camera).target).x AS tx, " + + "((camera).target).y AS ty, " + + "((camera).target).z AS tz, " + + "time AS time " + + "FROM previousnextclicked WHERE user_id = $1 ORDER BY time;", + [self.id], + function(err, result) { + self.results.previousNext = []; + for (var i in result.rows) { + self.results.previousNext.push( + { + type: 'previousnext', + time: result.rows[i].time, + previous: result.rows[i].previousnext == 'p', + position : { + x: result.rows[i].px, + y: result.rows[i].py, + z: result.rows[i].pz + }, + target : { + x: result.rows[i].tx, + y: result.rows[i].ty, + z: result.rows[i].tz + } + } + ); + } + self.ready.previousNext = true; + self.tryMerge(); + } + ); +} + +Info.prototype.loadHovered = function() { + var self = this; + this.client.query( + "SELECT start, time, arrow_id FROM hovered WHERE user_id = $1 ORDER BY time;", + [self.id], + function(err, result) { + self.results.hovered = []; + for (var i in result.rows) { + self.results.hovered.push( + { + type: "hovered", + time: result.rows[i].time, + start: result.rows[i].start, + id: result.rows[i].arrow_id + } + ); + } + self.ready.hovered = true; + self.tryMerge(); + } + ); +} + +var IdCreator = function(finishAction) { + this.finishAction = finishAction; + + // Connect to db + var self = this; + pg.connect(pgc.url, function(err, client, release) { + self.client = client; + self.release = release; + self.execute(); + }); +} + +IdCreator.prototype.execute = function() { + var self = this; + this.client.query( + "INSERT INTO users(name) VALUES('anonymous'); SELECT currval('users_id_seq');", + [], + function(err, result) { + self.finalResult = result.rows[0].currval; + self.finish(); + } + ); +} + +IdCreator.prototype.finish = function() { + this.release(); + this.client = null; + this.release = null; + + this.finishAction(this.finalResult); +} + +var IdChecker = function(id, finishAction) { + this.id = id; + this.finishAction = finishAction; + + var self = this; + pg.connect(pgc.url, function(err, client, release) { + self.client = client; + self.release = release; + self.execute(); + }); +} + +IdChecker.prototype.execute = function() { + var self = this; + this.client.query( + "SELECT count(id) > 0 AS answer FROM users WHERE id = $1;", + [self.id], + function(err, result) { + self.finalResult = result.rows[0].answer; + self.finish(); + } + ); +} + +IdChecker.prototype.finish = function() { + this.release(); + this.client = null; + this.release = null; + + this.finishAction(this.finalResult); +} + +var UserGetter = function(finishAction) { + this.finishAction = finishAction; + + var self = this; + pg.connect(pgc.url, function(err, client, release) { + self.client = client; + self.release = release; + self.execute(); + }); +} + +UserGetter.prototype.execute = function() { + var self = this; + this.client.query( + "SELECT id, name FROM users", + [], + function(err, result) { + self.finalResult = result.rows; + self.finish(); + } + ); +} + +UserGetter.prototype.finish = function() { + this.release(); + this.client = null; + this.release = null; + + this.finishAction(this.finalResult); +} + +module.exports.getInfo = function(id, callback) { new Info(id, callback); }; +module.exports.createId = function(callback) { new IdCreator(callback); }; +module.exports.checkId = function(id, callback) { new IdChecker(id, callback); }; +module.exports.getAllUsers = function(callback) { new UserGetter(callback); }; diff --git a/controllers/prototype/index.js b/controllers/prototype/index.js index a01449a..e13cff5 100644 --- a/controllers/prototype/index.js +++ b/controllers/prototype/index.js @@ -1,207 +1,7 @@ -var tools = require('../../my_modules/filterInt.js'); +var tools = require('../../my_modules/filterInt'); var pg = require('pg'); -var pgc = require('../../private.js'); - -var createNewId = function(req, res, callback) { - pg.connect(pgc.url, function(err, client, release) { - client.query( - "INSERT INTO users(name) VALUES('anonymous'); SELECT currval('users_id_seq');", - [], - function(err, result) { - req.session.user_id = result.rows[0].currval; - req.session.save(); - callback(); - release(); - } - ); - }); -} - -var checkId = function(req, res, next, callback, id) { - pg.connect(pgc.url, function(err, client, release) { - client.query( - "SELECT id FROM users WHERE id = $1", - [id], - function(err, result) { - if (result.rows.length > 0) { - callback(); - } else { - var error = new Error("Id not found"); - error.status = 404; - next(error); - } - release(); - } - ); - }); -} - -var addCamerasFromId = function(client, req, res, callback, id) { - client.query( - "SELECT ((camera).position).x AS px, " + - "((camera).position).y AS py, " + - "((camera).position).z AS pz, " + - "((camera).target).x AS tx, " + - "((camera).target).y AS ty, " + - "((camera).target).z AS tz, " + - "time AS time " + - "FROM keyboardevent WHERE user_id = $1 ORDER BY time;", - [id], - function(err, result) { - res.locals.path = res.locals.path || []; - for (var i in result.rows) { - res.locals.path.push( - { - type: 'camera', - position : { - x: result.rows[i].px, - y: result.rows[i].py, - z: result.rows[i].pz - }, - target : { - x: result.rows[i].tx, - y: result.rows[i].ty, - z: result.rows[i].tz - }, - time: result.rows[i].time - } - ); - } - callback(); - } - ); -} - -var addCoinsFromId = function(client, req, res, callback, id) { - client.query( - "SELECT coin_id, time FROM coinclicked WHERE user_id = $1", - [id], - function(err,result) { - res.locals.path = res.locals.path || []; - for (var i in result.rows) { - res.locals.path.push( - { - type: 'coin', - time: result.rows[i].time, - id: result.rows[i].coin_id - } - ); - } - callback(); - } - ); -} - -var addArrowsFromId = function(client, req, res, callback, id) { - client.query( - "SELECT arrow_id, time FROM arrowclicked WHERE user_id = $1", - [id], - function(err, result) { - res.locals.path = res.locals.path || []; - for (var i in result.rows) { - res.locals.path.push( - { - type: 'arrow', - time: result.rows[i].time, - id: result.rows[i].arrow_id - } - ); - } - callback(); - } - ); -} - -var addResetsFromId = function(client, req, res, callback, id) { - client.query( - "SELECT time FROM resetclicked WHERE user_id = $1", - [id], - function(err, result) { - res.locals.path = res.locals.path || []; - for (var i in result.rows) { - res.locals.path.push( - { - type: 'reset', - time: result.rows[i].time - } - ); - } - callback(); - } - ); -} - -var addPreviousNextFromId = function(client, req, res, callback, id) { - client.query( - "SELECT ((camera).position).x AS px, " + - "((camera).position).y AS py, " + - "((camera).position).z AS pz, " + - "((camera).target).x AS tx, " + - "((camera).target).y AS ty, " + - "((camera).target).z AS tz, " + - "time AS time " + - "FROM previousnextclicked WHERE user_id = $1;", - [id], - function(err, result) { - res.locals.path = res.locals.path || []; - for (var i in result.rows) { - res.locals.path.push( - { - type: 'previousnext', - time: result.rows[i].time, - previous: result.rows[i].previousnext == 'p', - position : { - x: result.rows[i].px, - y: result.rows[i].py, - z: result.rows[i].pz - }, - target : { - x: result.rows[i].tx, - y: result.rows[i].ty, - z: result.rows[i].tz - } - } - ); - } - callback(); - } - ); -} - -var addHoveredFromId = function(client, req, res, callback, id) { - client.query( - "SELECT start, time, arrow_id FROM hovered WHERE user_id = $1", - [id], - function(err, result) { - res.locals.path = res.locals.path || []; - for (var i in result.rows) { - res.locals.path.push( - { - type: "hovered", - time: result.rows[i].time, - start: result.rows[i].start, - id: result.rows[i].arrow_id - } - ); - } - callback(); - } - ); -} - -var getAllUsers = function(req, res, callback) { - pg.connect(pgc.url, function(err, client, release) { - client.query( - "SELECT id, name FROM users;", - [], - function(err, result) { - res.locals.ids = result.rows; - callback(); - release(); - } - ); - }); -} +var pgc = require('../../private'); +var db = require('./dbrequests'); module.exports.index = function(req, res) { res.setHeader('Content-Type', 'text/html'); @@ -211,41 +11,23 @@ module.exports.index = function(req, res) { }); } -module.exports.arrows = function(req, res) { - createNewId(req, res, function() { - res.setHeader('Content-Type', 'text/html'); +var protoHelper = function(template) { + return function(req, res) { + db.createId(function(id) { + req.session.user_id = id; + req.session.save(); - res.locals.cameraStyle = 'arrows'; - - res.render('prototype.jade', res.locals, function(err, result) { - res.send(result); + res.setHeader('Content-Type','text/html'); + res.render(template, res.locals, function(err, result) { + res.send(result); + }); }); - }); + }; } -module.exports.viewports = function(req, res) { - createNewId(req, res, function() { - res.setHeader('Content-Type', 'text/html'); - - res.locals.cameraStyle = 'viewports'; - - res.render('prototype.jade', res.locals, function(err, result) { - res.send(result); - }); - }); -} - -module.exports.reverse = function(req, res) { - createNewId(req, res, function() { - res.setHeader('Content-Type', 'text/html'); - - res.locals.cameraStyle = 'reverse'; - - res.render('prototype.jade', res.locals, function(err, result) { - res.send(result); - }); - }); -} +module.exports.arrows = protoHelper('prototype_arrows.jade'); +module.exports.viewports = protoHelper('prototype_viewports.jade'); +module.exports.reverse = protoHelper('prototype_reverse.jade'); module.exports.replay_info = function(req, res) { res.setHeader('Content-Type', 'text/plain'); @@ -253,46 +35,34 @@ module.exports.replay_info = function(req, res) { // Parse id var id = tools.filterInt(req.params.id); - pg.connect(pgc.url, function(err, client, release) { - addCamerasFromId(client, req, res, function() { - addCoinsFromId(client, req, res, function() { - addArrowsFromId(client, req, res, function() { - addResetsFromId(client, req, res, function() { - addPreviousNextFromId(client, req, res, function() { - addHoveredFromId(client, req, res, function() { - res.locals.path.sort(function(elt1, elt2) { - // Dates as string can be compared - if (elt1.time < elt2.time) - return -1; - if (elt1.time > elt2.time) - return 1; - return 0; - }); - res.send(JSON.stringify(res.locals.path)); - }, id); - }, id); - }, id); - }, id); - }, id); - }, id); - release(); + db.getInfo(id, function(results) { + res.send(JSON.stringify(results)); }); } module.exports.replay = function(req, res, next) { + // Get id parameter res.locals.id = tools.filterInt(req.params.id); - checkId(req,res, next, function() { - res.setHeader('Content-Type', 'text/html'); - res.locals.cameraStyle = "replay"; - res.render('prototype.jade', res.locals, function(err, result) { - res.send(result); - }); - }, res.locals.id); + db.checkId(res.locals.id, function(idExist) { + if (!idExist) { + var err = new Error("This replay does not exist"); + err.status = 404; + next(err); + } else { + res.setHeader('Content-Type', 'text/html'); + res.render('prototype_replays.jade', res.locals, function(err, result) { + res.send(result); + }); + } + }); } module.exports.replay_index = function(req, res, next) { - getAllUsers(req, res, function() { + db.getAllUsers(function(result) { + res.locals.users = result; + + res.setHeader('Content-Type', 'text/html'); res.render("replay_index.jade", res.locals, function(err, result) { res.send(result); }); diff --git a/controllers/prototype/views/prototype.jade b/controllers/prototype/views/prototype.jade index 838b84e..fb82db6 100644 --- a/controllers/prototype/views/prototype.jade +++ b/controllers/prototype/views/prototype.jade @@ -15,20 +15,8 @@ block extrajs script(src="/static/js/prototype/Coin.js") script(src="/static/js/Logger.js") - if cameraStyle == 'replay' - script var params = params || {}; params.get = params.get || {}; params.get.id = #{id}; - script(src="/static/js/ReplayCamera.js") - script(src="/static/js/prototype/replay.js") - else - if cameraStyle == 'arrows' - script RecommendedCamera = FixedCamera; - else if cameraStyle == 'viewports' - script RecommendedCamera = OldFixedCamera; - else if cameraStyle == 'reverse' - script RecommendedCamera = ReverseCamera - - script(src="/static/js/prototype/main.js") - + block configjs + block mainjs script document.getElementById('music').volume = 0.5; block extrahead @@ -36,75 +24,7 @@ block extrahead block content #main-div.panel-group(style={'margin-top':'10px', 'margin-bottom':'10px'}) - if cameraStyle != 'replay' - #instructions.panel.panel-default - .panel-heading - h4.panel-title - a(href="#", data-target="#collapseInstructions", data-toggle="collapse",onclick="setTimeout(onWindowResize,500);") Instructions - - .panel-collapse.collapse.in#collapseInstructions - .panel-body - p - | This is the prototype of a 3D interface. You can move - | the camera with the arrow keys of your keyboard (or - | WASD if you like FPS-games, and by the way, if you - | use azerty keyboard, you can also use ZQSD instead), - | and change the angle of the camera by dragging and - | dropping the scene around it (you can also use your - | numpad, 2 to look lower, 8 to look higher, 4 to look - | on the left and 6 to look on the right, but if you're - | more comfortable with non-numpad keys, you can also - | use i for up, j for left, k for down, and l for - | right). - - p - | This is a re-creation of the Bob-omb Battlefield - | level from Super Mario 64, and with its 8 red coins. - | You can click on them to get them, and once you got - | them all, you get... well you get nothing but the - | sound of the star is played (but the star doesn't - | appear... sorry guys) - - p - | It contains recommended views : 3D objects - | here to guide you through this coin search. - - if cameraStyle == 'arrows' - - p - | Recommended views are displayed with a - | transparent blue arrow. They disappear when you - | come closer to them, and shows the motion between - | your current position and the recommendation. - - else if cameraStyle == 'viewports' - - p - | Recommended views are displayed with a - | transparent red rectangle and some lines. - | Basically, it represents the position of a camera - | (the point at the extramities of the lines - | represents the optical center, and the red - | rectangle represents the image plane). - - else if cameraStyle == 'reverse' - - p - | Recommended views are displayed with a strange - | blue object. Basically, the curve at the - | begining of this weird object shows the motion - | that starts from where you are and leads to the - | recommended view, and the extremity is in fact an - | object representing a camera. - - p - | You can click on a recommendation to move to the - | recommended viewpoint. The recommendation will become - | more and more transparent as you come closer, and - | will disappear when you reach it. - - p - | You may now hide this panel and start playing ! + block description button#full.btn.btn-primary(style={'margin-right': '10px', 'margin-bottom': '10px'}) Fullscreen button#reset.btn.btn-primary(style={'margin-right': '10px', 'margin-bottom':'10px'}) Reset camera diff --git a/controllers/prototype/views/prototype_arrows.jade b/controllers/prototype/views/prototype_arrows.jade new file mode 100644 index 0000000..8c3c656 --- /dev/null +++ b/controllers/prototype/views/prototype_arrows.jade @@ -0,0 +1,14 @@ +extends ./prototype_interactive + +block title + title #{title} - Prototype - Arrows + +block configjs + script RecommendedCamera = FixedCamera; + +block preciseDescription + p + | Recommended views are displayed with a + | transparent blue arrow. They disappear when you + | come closer to them, and shows the motion between + | your current position and the recommendation. diff --git a/controllers/prototype/views/prototype_interactive.jade b/controllers/prototype/views/prototype_interactive.jade new file mode 100644 index 0000000..69f4315 --- /dev/null +++ b/controllers/prototype/views/prototype_interactive.jade @@ -0,0 +1,51 @@ +extends ./prototype + +block title + title #{title} - Prototype + +block mainjs + script(src="/static/js/prototype/main.js") + +block description + #instructions.panel.panel-default + .panel-heading + h4.panel-title + a(href="#", data-target="#collapseInstructions", data-toggle="collapse",onclick="setTimeout(onWindowResize,500);") Instructions + + .panel-collapse.collapse.in#collapseInstructions + .panel-body + p + | This is the prototype of a 3D interface. You can move + | the camera with the arrow keys of your keyboard (or + | WASD if you like FPS-games, and by the way, if you + | use azerty keyboard, you can also use ZQSD instead), + | and change the angle of the camera by dragging and + | dropping the scene around it (you can also use your + | numpad, 2 to look lower, 8 to look higher, 4 to look + | on the left and 6 to look on the right, but if you're + | more comfortable with non-numpad keys, you can also + | use i for up, j for left, k for down, and l for + | right). + + p + | This is a re-creation of the Bob-omb Battlefield + | level from Super Mario 64, and with its 8 red coins. + | You can click on them to get them, and once you got + | them all, you get... well you get nothing but the + | sound of the star is played (but the star doesn't + | appear... sorry guys) + + p + | It contains recommended views : 3D objects + | here to guide you through this coin search. + + block preciseDescription + + p + | You can click on a recommendation to move to the + | recommended viewpoint. The recommendation will become + | more and more transparent as you come closer, and + | will disappear when you reach it. + + p + | You may now hide this panel and start playing ! diff --git a/controllers/prototype/views/prototype_replays.jade b/controllers/prototype/views/prototype_replays.jade new file mode 100644 index 0000000..ba28af7 --- /dev/null +++ b/controllers/prototype/views/prototype_replays.jade @@ -0,0 +1,12 @@ +extends ./prototype + +block title + title #{title} - Prototype - Replay + +block configjs + script RecommendedCamera = FixedCamera; + script var params = params || {}; params.get = params.get || {}; params.get.id = #{id}; + script(src="/static/js/ReplayCamera.js") + +block mainjs + script(src="/static/js/prototype/replay.js") diff --git a/controllers/prototype/views/prototype_reverse.jade b/controllers/prototype/views/prototype_reverse.jade new file mode 100644 index 0000000..4fe3d7f --- /dev/null +++ b/controllers/prototype/views/prototype_reverse.jade @@ -0,0 +1,17 @@ +extends ./prototype_interactive + +block title + title #{title} - Prototype - Reverse + +block configjs + script RecommendedCamera = ReverseCamera; + +block preciseDescription + p + | Recommended views are displayed with a strange + | blue object. Basically, the curve at the + | begining of this weird object shows the motion + | that starts from where you are and leads to the + | recommended view, and the extremity is in fact an + | object representing a camera. + diff --git a/controllers/prototype/views/prototype_viewports.jade b/controllers/prototype/views/prototype_viewports.jade new file mode 100644 index 0000000..71f69f8 --- /dev/null +++ b/controllers/prototype/views/prototype_viewports.jade @@ -0,0 +1,16 @@ +extends ./prototype_interactive + +block title + title #{title} - Prototype - Viewports + +block configjs + script RecommendedCamera = OldFixedCamera; + +block preciseDescription + p + | Recommended views are displayed with a + | transparent red rectangle and some lines. + | Basically, it represents the position of a camera + | (the point at the extramities of the lines + | represents the optical center, and the red + | rectangle represents the image plane). diff --git a/controllers/prototype/views/replay_index.jade b/controllers/prototype/views/replay_index.jade index 6a9e49c..92e806f 100644 --- a/controllers/prototype/views/replay_index.jade +++ b/controllers/prototype/views/replay_index.jade @@ -5,9 +5,9 @@ block title block content h2 Replays available - if ids.length == 0 + if users.length == 0 p Sorry, there are no replays available... try later or do an experiment ! else ol - each elt in ids + each elt in users li #{elt.name}