From 0a6dc82e754d8c099ec53d16331cc24dd28c0d6f Mon Sep 17 00:00:00 2001
From: sonsoleslp
Date: Tue, 4 May 2021 15:47:32 +0200
Subject: [PATCH] Add non-solved puzzles record
---
controllers/analytics_controller.js | 75 ++++++++++++++++---
controllers/escapeRoom_controller.js | 3 +-
controllers/play_controller.js | 1 +
helpers/analytics.js | 26 ++++++-
helpers/utils.js | 57 +++++++++++---
i18n/en.json | 4 +
i18n/es.json | 6 +-
...ntFromInvitationFieldInEscapeRoomsTable.js | 1 -
...2910005-AddSuccessToRetosSuperadosTable.js | 10 +++
...RemoveConstraintFromRetosSuperadosTable.js | 4 +
...50310005-AddAnswerToRetosSuperadosTable.js | 10 +++
models/index.js | 22 +++++-
models/retosSuperados.js | 9 +++
models/user.js | 4 +-
public/js/play.js | 1 +
public/stylesheets/style.css | 34 ++++++++-
public/stylesheets/timeline.sass | 35 ++++++++-
queries/team.js | 59 ++++++++++++++-
queries/user.js | 5 +-
.../20190208221150-FillEscapeRoomsTable.js | 2 +-
test/sockets.example.html | 1 -
views/escapeRooms/analytics/puzzleStats.ejs | 14 +++-
views/escapeRooms/analytics/timeline.ejs | 54 ++++++++++---
views/escapeRooms/showStudent.ejs | 4 +-
24 files changed, 386 insertions(+), 55 deletions(-)
create mode 100644 migrations/2021042910005-AddSuccessToRetosSuperadosTable.js
create mode 100644 migrations/20210429120005-RemoveConstraintFromRetosSuperadosTable.js
create mode 100644 migrations/2021050310005-AddAnswerToRetosSuperadosTable.js
create mode 100644 models/retosSuperados.js
diff --git a/controllers/analytics_controller.js b/controllers/analytics_controller.js
index be964ab7..7dd7bb86 100644
--- a/controllers/analytics_controller.js
+++ b/controllers/analytics_controller.js
@@ -1,7 +1,7 @@
const {models} = require("../models");
const {createCsvFile} = require("../helpers/csv");
const queries = require("../queries");
-const {flattenObject, getERPuzzles, getERTurnos} = require("../helpers/utils");
+const {flattenObject, getERPuzzles, getERTurnos, groupByTeamRetos} = require("../helpers/utils");
const {isTeamConnected, isTeamConnectedWaiting} = require("../helpers/sockets");
const {convertDate, retosSuperadosByWho, getRetosSuperados, getBestTime, getAvgHints, byRanking, pctgRetosSuperados, getRetosSuperadosIdTime, countHints, countHintsByPuzzle} = require("../helpers/analytics");
const stats = require("../helpers/stats");
@@ -317,7 +317,9 @@ exports.timeline = async (req, res, next) => {
try {
escapeRoom.turnos = await getERTurnos(escapeRoom.id);
escapeRoom.puzzles = await getERPuzzles(escapeRoom.id);
- escapeRoom.teams = await models.team.findAll(queries.team.teamComplete(escapeRoom.id, turnId, "lower(team.name) ASC"));
+ escapeRoom.teams = await models.team.findAll(queries.team.teamComplete(escapeRoom.id, turnId, "lower(team.name) ASC", false, true));
+ escapeRoom.retosNoSuperados = groupByTeamRetos(await models.team.findAll(queries.team.teamRetosNoSuperados(escapeRoom.id, turnId)));
+
for (const team of escapeRoom.teams) {
team.connected = isTeamConnected(team.id);
team.waiting = team.connected ? false : isTeamConnectedWaiting(team.id);
@@ -335,8 +337,10 @@ exports.puzzleStats = async (req, res, next) => {
const {turnId} = query;
const resultSingle = {};
const result = {};
+ const resultNo = {};
const summary = {};
const summarySingle = {};
+ const summaryNo = {};
try {
escapeRoom.puzzles = await getERPuzzles(escapeRoom.id);
@@ -359,11 +363,21 @@ exports.puzzleStats = async (req, res, next) => {
return {"id": team.id, retosSuperados};
});
+ const retosNoSuperados = groupByTeamRetos(await models.team.findAll(queries.team.teamRetosNoSuperados(escapeRoom.id, turnId)), true);
+
+ for (const t in retosNoSuperados) {
+ const team = retosNoSuperados[t];
+
+ for (const p in team) {
+ resultNo[p] = [...resultNo[p] || [], team[p].length];
+ }
+ }
for (const p of escapeRoom.puzzles) {
const puzzleId = p.id;
const resultSinglePuzzle = resultSingle[puzzleId] || [];
const resultPuzzle = result[puzzleId] || [];
+ const resultNoPuzzle = resultNo[puzzleId] || [];
summarySingle[puzzleId] = {
"min": stats.min(resultSinglePuzzle),
@@ -380,8 +394,17 @@ exports.puzzleStats = async (req, res, next) => {
"median": stats.median(resultPuzzle),
"std": stats.std(resultPuzzle)
};
+
+ summaryNo[puzzleId] = {
+ "count": stats.count(resultNoPuzzle),
+ "min": stats.min(resultNoPuzzle),
+ "max": stats.max(resultNoPuzzle),
+ "mean": stats.mean(resultNoPuzzle),
+ "median": stats.median(resultNoPuzzle),
+ "std": stats.std(resultNoPuzzle)
+ };
}
- res.render("escapeRooms/analytics/puzzleStats", {"escapeRoom": req.escapeRoom, "puzzles": result, turnId, summary, summarySingle});
+ res.render("escapeRooms/analytics/puzzleStats", {"escapeRoom": req.escapeRoom, "puzzles": result, turnId, summary, summarySingle, summaryNo});
} catch (e) {
console.error(e);
next(e);
@@ -516,6 +539,7 @@ exports.download = async (req, res) => {
const puzzleIds = puzzles.map((puz) => puz.id);
const puzzleNames = puzzles.map((puz) => puz.title);
const users = await models.user.findAll(queries.user.puzzlesByParticipant(escapeRoom.id, turnId, orderBy, true, true));
+ const retosNoSuperados = groupByTeamRetos(await models.team.findAll(queries.team.teamRetosNoSuperados(escapeRoom.id, turnId)));
const results = users.map((user) => {
const {name, surname, username} = user;
@@ -523,8 +547,14 @@ exports.download = async (req, res) => {
const turnoTag = user.turnosAgregados[0].place;
const teamAttendance = Boolean(user.teamsAgregados[0].startTime);
const [{"name": teamName, "id": teamId, requestedHints}] = user.teamsAgregados;
-
+ const teamNoSuperados = [];
const {retosSuperados, retosSuperadosMin} = retosSuperadosByWho(user.teamsAgregados[0], puzzleIds, true, turno);
+
+ for (let i = 0; i < puzzleIds.length; i++) {
+ teamNoSuperados.push(((retosNoSuperados[teamId] || [])[i] || []).length);
+ }
+
+ const rns = flattenObject(teamNoSuperados, puzzleNames.map((p) => `Failed attempts to solve ${p}`));
const rs = flattenObject(retosSuperados, puzzleNames);
const rsMin = flattenObject(retosSuperadosMin, puzzleNames, true);
@@ -533,7 +563,7 @@ exports.download = async (req, res) => {
const hs = flattenObject(hintsSucceeded, puzzleNames.map((p) => `Hints succeeded for ${p}`));
const attendance = Boolean(user.turnosAgregados[0].participants.attendance);
- return {name, surname, username, teamId, teamName, attendance, teamAttendance, ...rs, ...rsMin, turnoTag, turno, hintsFailedTotal, ...hf, hintsSucceededTotal, ...hs};
+ return {name, surname, username, teamId, teamName, attendance, teamAttendance, ...rns, ...rs, ...rsMin, turnoTag, turno, hintsFailedTotal, ...hf, hintsSucceededTotal, ...hs};
});
createCsvFile(res, results);
@@ -557,8 +587,10 @@ exports.downloadRaw = async (req, res) => {
}
const logs = [];
const teams = await models.team.findAll(queries.team.puzzlesByTeam(escapeRoom.id, turnId, true));
+ const retosNoSuperados = groupByTeamRetos(await models.team.findAll(queries.team.teamRetosNoSuperados(escapeRoom.id, turnId)));
- for (const team of teams) {
+ for (const t in teams) {
+ const team = teams[t];
const {id, name, teamMembers, requestedHints, retos} = team;
const startTime = team.turno.startTime || team.startTime;
const logBase = {
@@ -577,13 +609,31 @@ exports.downloadRaw = async (req, res) => {
"hintCategory": "",
"hintQuizScore": "",
"puzzleId": "",
- "puzzleName": ""
+ "puzzleName": "",
+ "puzzleSol": "",
+ "puzzleSubmittedAnswer": ""
};
for (const r in retos) {
const retoTS = retos[r].retosSuperados.createdAt;
-
+ const submittedAnswer = retos[r].retosSuperados.answer === "" ? retos[r].sol : retos[r].retosSuperados.answer;
+
+ if (retosNoSuperados[id] && retosNoSuperados[id][retos[r].order]) {
+ for (const rns of retosNoSuperados[id][retos[r].order]) {
+ logs.push({
+ ...logBase,
+ "timestamp": convertDate(rns.when),
+ "minute": Math.round(100 * (rns.when - startTime) / 1000 / 60) / 100,
+ "event": "PUZZLE_FAILED_TO_SOLVE",
+ "puzzleId": retos[r].order + 1,
+ "puzzleName": retos[r].title,
+ "puzzleSol": retos[r].sol,
+ "puzzleSubmittedAnswer": rns.answer,
+ "eventComplete": `PUZZLE_FAILED_TO_SOLVE_${retos[r].order + 1}`
+ });
+ }
+ }
logs.push({
...logBase,
"event": "PUZZLE_SOLVED",
@@ -591,13 +641,16 @@ exports.downloadRaw = async (req, res) => {
"minute": Math.round(100 * (retoTS - startTime) / 1000 / 60) / 100,
"puzzleId": retos[r].order + 1,
"puzzleName": retos[r].title,
+ "puzzleSol": retos[r].sol,
+ "puzzleSubmittedAnswer": submittedAnswer,
"eventComplete": `PUZZLE_SOLVED_${retos[r].order + 1}`
});
}
for (const h of requestedHints) {
const hintTS = h.createdAt;
- const hintId = h.hint ? `${puzzleIdToOrder[h.hint.puzzleId]}.${h.hint.order + 1}` : "";
+ const puzId = puzzleIdToOrder[h.hint.puzzleId];
+ const hintId = h.hint ? `${puzId}.${h.hint.order + 1}` : "";
logs.push({
...logBase,
@@ -608,7 +661,9 @@ exports.downloadRaw = async (req, res) => {
"hintCategory": h.hint ? h.hint.category || "" : "",
"hintContent": h.hint ? h.hint.content : "",
"hintQuizScore": parseInt(h.score, 10),
- "eventComplete": h.success ? `HINT_OBTAINED_${hintId || "CUSTOM"}` : "HINT_FAILED_TO_OBTAIN"
+ "eventComplete": h.success ? `HINT_OBTAINED_${hintId || "CUSTOM"}` : "HINT_FAILED_TO_OBTAIN",
+ "puzzleId": puzId,
+ "puzzleName": escapeRoom.puzzles[puzId].title
});
}
}
diff --git a/controllers/escapeRoom_controller.js b/controllers/escapeRoom_controller.js
index c6782c4e..3968c3e4 100644
--- a/controllers/escapeRoom_controller.js
+++ b/controllers/escapeRoom_controller.js
@@ -97,7 +97,7 @@ exports.show = async (req, res) => {
if (participant) {
const [team] = participant.teamsAgregados;
- const howManyRetos = await team.countRetos();
+ const howManyRetos = await models.retosSuperados.count({"where": {"success": true, "teamId": team.id }});
const finished = howManyRetos === escapeRoom.puzzles.length;
res.render("escapeRooms/showStudent", {escapeRoom, cloudinary, participant, team, finished});
@@ -196,7 +196,6 @@ exports.update = async (req, res) => {
escapeRoom.forceLang = body.forceLang === "en" || body.forceLang === "es" ? body.forceLang : null;
const progressBar = body.progress;
- console.log(body);
try {
const er = await escapeRoom.save({"fields": ["title", "subject", "duration", "forbiddenLateSubmissions", "description", "scope", "teamSize", "supportLink", "forceLang", "invitation"]});
diff --git a/controllers/play_controller.js b/controllers/play_controller.js
index 0591be13..92a4ecad 100644
--- a/controllers/play_controller.js
+++ b/controllers/play_controller.js
@@ -12,6 +12,7 @@ exports.play = (req, res, next) => playInterface("team", req, res, next);
// GET /escapeRooms/:escapeRoomId/project
exports.classInterface = (req, res, next) => playInterface("class", req, res, next);
+// GET /escapeRooms/:escapeRoomId/ranking
exports.ranking = async (req, _res, next) => {
let {turnoId} = req.params;
diff --git a/helpers/analytics.js b/helpers/analytics.js
index 7d55be3f..d0c2b8bb 100644
--- a/helpers/analytics.js
+++ b/helpers/analytics.js
@@ -1,3 +1,6 @@
+const sequelize = require("../models");
+const {models} = sequelize;
+
exports.retosSuperadosByWho = (who, puzzles, showDate = false, turno) => {
const retosSuperados = new Array(puzzles.length).fill(0);
const retosSuperadosMin = new Array(puzzles.length).fill(0);
@@ -35,13 +38,30 @@ exports.getRetosSuperados = (teams, nPuzzles, ignoreTurno = false) => teams.
exports.getRetosSuperadosIdTime = (retos, actualStartTime) => retos.map((reto) => {
const {retosSuperados} = reto;
- const {createdAt} = retosSuperados;
+ const {createdAt, success} = retosSuperados;
const time = actualStartTime ? Math.floor((createdAt - actualStartTime) / 10) / 100 : null;
- return {"id": reto.id, time};
+ return {"id": reto.id, time, success};
});
+
exports.getPuzzleOrderSuperados = async (team) => {
- const retosSuperados = await team.getRetos({ "attributes": ["order", "title", "correct", "sol", "score"], "order": [["order", "ASC"]]});
+ const retosSuperados = await models.puzzle.findAll({
+ "attributes": ["order", "title", "correct", "sol", "score"],
+ "include": [
+ {
+ "model": models.team,
+ "as": "superados",
+ "where": {"id": team.id},
+ "through": {
+ "model": models.retosSuperados,
+ "where": {"success": true},
+ "required": true
+ }
+ }
+ ],
+ "order": [["order", "ASC"]]
+ });
+
const puzzleData = {};
const puzzlesSolved = retosSuperados.length ? retosSuperados.map((r) => {
const order = r.order + 1;
diff --git a/helpers/utils.js b/helpers/utils.js
index 792e8e27..5a0fd20f 100644
--- a/helpers/utils.js
+++ b/helpers/utils.js
@@ -91,6 +91,7 @@ exports.playInterface = async (name, req, res, next) => {
"through": {
"model": models.retosSuperados,
"required": false,
+ "where": {"success": true},
"attributes": ["createdAt"]
}
}
@@ -257,7 +258,6 @@ exports.checkPuzzle = async (solution, puzzle, escapeRoom, teams, user, i18n, pu
try {
correctAnswer = removeDiacritics(answer.toString().toLowerCase().trim()) === removeDiacritics(puzzleSol.toString().toLowerCase().trim());
- console.log(removeDiacritics(answer.toString().toLowerCase().trim()));
if (correctAnswer) {
msg = puzzle.correct || i18n.escapeRoom.play.correct;
} else {
@@ -267,15 +267,20 @@ exports.checkPuzzle = async (solution, puzzle, escapeRoom, teams, user, i18n, pu
const participationCode = await exports.checkTurnoAccess(teams, user, escapeRoom, puzzleOrder);
participation = participationCode;
- alreadySolved = await puzzle.hasSuperado(teams[0].id);
-
- if (participation === PARTICIPANT && correctAnswer) {
+ alreadySolved = Boolean(await models.retosSuperados.findOne({"where": {"puzzleId": puzzle.id, "teamId": teams[0].id, "success": true}}));
+ if (participation === PARTICIPANT) {
try {
- code = OK;
- if (!alreadySolved) {
- await puzzle.addSuperados(teams[0].id);
+ if (correctAnswer) {
+ code = OK;
+ if (!alreadySolved) {
+ await models.retosSuperados.create({"puzzleId": puzzle.id, "teamId": teams[0].id, "success": true, answer});
+ }
+ } else {
+ await models.retosSuperados.create({"puzzleId": puzzle.id, "teamId": teams[0].id, "success": false, answer});
+ status = 423;
}
} catch (e) {
+ console.error(e);
code = ERROR;
status = 500;
msg = e.message;
@@ -349,7 +354,23 @@ exports.checkIsTurnAvailable = (turn, duration) => {
};
exports.getCurrentPuzzle = async (team, puzzles) => {
- const retosSuperados = await team.getRetos();
+ const retosSuperados = await models.puzzle.findAll({
+ "attributes": ["order", "title", "correct", "sol", "score"],
+ "include": [
+ {
+ "model": models.team,
+ "as": "superados",
+ "where": {"id": team.id},
+ "through": {
+ "model": models.retosSuperados,
+ "where": {"success": true},
+ "required": true
+ }
+ }
+ ],
+ "order": [["order", "ASC"]]
+ });
+
const retosSuperadosOrder = retosSuperados.map((r) => r.order);
const pending = puzzles.map((p) => p.order).filter((p) => retosSuperadosOrder.indexOf(p) === -1);
let currentlyWorkingOn = retosSuperadosOrder.length ? Math.max(...retosSuperadosOrder) + 1 : 0;
@@ -382,9 +403,7 @@ exports.areHintsAllowedForTeam = async (teamId, hintLimit) => {
return {hintsAllowed, successHints, failHints};
};
-exports.getContentForPuzzle = (content = "[]", currentlyWorkingOn) => {
- return JSON.parse(content || "[]").map((block, index) => ({...block, index})).filter((block) => block.puzzles.indexOf(currentlyWorkingOn.toString()) !== -1);
-}
+exports.getContentForPuzzle = (content = "[]", currentlyWorkingOn) => JSON.parse(content || "[]").map((block, index) => ({...block, index})).filter((block) => block.puzzles.indexOf(currentlyWorkingOn.toString()) !== -1);
exports.paginate = (page = 1, pages, limit = 5) => {
let from = 0;
let to = 0;
@@ -424,3 +443,19 @@ exports.validationError = ({instance, path, validatorKey}, i18n) => {
exports.isValidDate = (d) => d === null || d instanceof Date && !isNaN(d);
+exports.groupByTeamRetos = (retos, useIdInsteadOfOrder = false) => retos.reduce((acc, val) => {
+ const {id} = val;
+ const success = val["puzzlesSolved.success"];
+ const when = val["puzzlesSolved.createdAt"];
+ const answer = val["puzzlesSolved.answer"];
+ const order = useIdInsteadOfOrder ? val["puzzlesSolved.puzzle.id"] : val["puzzlesSolved.puzzle.order"];
+
+ if (!acc[id]) {
+ acc[id] = {[order]: [{success, when, answer}] };
+ } else if (!acc[id][order]) {
+ acc[id][order] = [{success, when, answer}];
+ } else {
+ acc[id][order].push({success, when, answer});
+ }
+ return acc;
+}, {});
diff --git a/i18n/en.json b/i18n/en.json
index 7b23566c..f9944d23 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -91,6 +91,8 @@
"hint": "Hint",
"score": "Score",
"solved": "Solved",
+ "failedToSolve": "Failed attempt to solve the puzzle",
+ "providedAnswer": "Provided answer",
"title": "Timeline",
"showLegend": "Show legend",
"time": "Time",
@@ -1053,6 +1055,8 @@
},
"hint": {
"Hints": "Hints",
+ "hintFailedToObtain": "Hint failed to obtain",
+ "hintObtained": "Hint obtained",
"attributes": {
"content": "The hint content",
"order": "The order",
diff --git a/i18n/es.json b/i18n/es.json
index 09616d7a..8fafcba9 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -65,7 +65,7 @@
},
"puzzleStats": {
"title": "Estadísticas de retos",
- "cumulative": "Tiempos acumuladis",
+ "cumulative": "Tiempos acumulados",
"nonCumulative": "Tiempos no acumulados"
},
"puzzleTimes": {
@@ -88,6 +88,8 @@
"hint": "Pista",
"score": "Puntuación",
"solved": "Resuelto",
+ "failedToSolve": "Intento fallido de resolver el reto",
+ "providedAnswer": "Respuesta introducida",
"time": "Tiempo",
"unsolved": "No resuelto",
"showLegend": "Mostrar leyenda",
@@ -1042,6 +1044,8 @@
},
"hint": {
"Hints": "Pistas",
+ "hintFailedToObtain": "Pista no obtenida",
+ "hintObtained": "Pista obtenida",
"attributes": {
"content": "El contenido de la pista",
"order": "El orden",
diff --git a/migrations/20210201110005-RemoveConstraintFromInvitationFieldInEscapeRoomsTable.js b/migrations/20210201110005-RemoveConstraintFromInvitationFieldInEscapeRoomsTable.js
index d476a9ad..2f8dfa2c 100644
--- a/migrations/20210201110005-RemoveConstraintFromInvitationFieldInEscapeRoomsTable.js
+++ b/migrations/20210201110005-RemoveConstraintFromInvitationFieldInEscapeRoomsTable.js
@@ -1,4 +1,3 @@
-
"use strict";
module.exports = {"up": (queryInterface) => queryInterface.removeConstraint("escapeRooms", "escapeRooms_invitation_key")};
diff --git a/migrations/2021042910005-AddSuccessToRetosSuperadosTable.js b/migrations/2021042910005-AddSuccessToRetosSuperadosTable.js
new file mode 100644
index 00000000..cabe6a32
--- /dev/null
+++ b/migrations/2021042910005-AddSuccessToRetosSuperadosTable.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ "up": (queryInterface, Sequelize) => queryInterface.addColumn("retosSuperados", "success", {
+ "type": Sequelize.BOOLEAN,
+ "defaultValue": true
+ }),
+ "down": (queryInterface) => queryInterface.removeColumn("retosSuperados", "success")
+};
+
diff --git a/migrations/20210429120005-RemoveConstraintFromRetosSuperadosTable.js b/migrations/20210429120005-RemoveConstraintFromRetosSuperadosTable.js
new file mode 100644
index 00000000..23d74dd0
--- /dev/null
+++ b/migrations/20210429120005-RemoveConstraintFromRetosSuperadosTable.js
@@ -0,0 +1,4 @@
+
+"use strict";
+
+module.exports = {"up": (queryInterface) => queryInterface.removeConstraint("retosSuperados", "retosSuperados_pkey")};
diff --git a/migrations/2021050310005-AddAnswerToRetosSuperadosTable.js b/migrations/2021050310005-AddAnswerToRetosSuperadosTable.js
new file mode 100644
index 00000000..497c1362
--- /dev/null
+++ b/migrations/2021050310005-AddAnswerToRetosSuperadosTable.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ "up": (queryInterface, Sequelize) => queryInterface.addColumn("retosSuperados", "answer", {
+ "type": Sequelize.TEXT,
+ "defaultValue": ""
+ }),
+ "down": (queryInterface) => queryInterface.removeColumn("retosSuperados", "answer")
+};
+
diff --git a/models/index.js b/models/index.js
index ba5602c0..8458871c 100644
--- a/models/index.js
+++ b/models/index.js
@@ -47,8 +47,12 @@ sequelize.import(path.join(__dirname, "app"));
// Import the definition of the Resource Table from app.js
sequelize.import(path.join(__dirname, "resource"));
+// Import the definition of the RetosSuperados Table from retosSuperados.js
+sequelize.import(path.join(__dirname, "retosSuperados"));
+
+
// Relation between models
-const {escapeRoom, turno, attachment, user, puzzle, hint, hintApp, team, requestedHint, asset, app, resource} = sequelize.models;// Relation 1-to-N between Escape Room and Turn:
+const {escapeRoom, turno, attachment, user, puzzle, hint, hintApp, team, requestedHint, retosSuperados, asset, app, resource} = sequelize.models;// Relation 1-to-N between Escape Room and Turn:
// Relation 1-to-N between Escape Room and Turno:
@@ -182,6 +186,7 @@ team.belongsToMany(puzzle, {
puzzle.belongsToMany(team, {
"as": "superados",
"through": "retosSuperados",
+ "unique": false,
"foreignKey": {
"name": "puzzleId",
"allowNull": false
@@ -191,11 +196,24 @@ puzzle.belongsToMany(team, {
"constraints": true
});
+// Relation N-to-M between Team and Puzzle:
+retosSuperados.belongsTo(team, {"unique": false, "foreignKey": "teamId"});
+retosSuperados.belongsTo(puzzle, {"unique": false, "foreignKey": "puzzleId"});
+
+team.hasMany(retosSuperados, {
+ "as": "puzzlesSolved",
+ "foreignKey": {
+ "name": "teamId",
+ "unique": false,
+ "allowNull": false
+ }
+});
+
// Relation N-to-M between Team and Hint:
requestedHint.belongsTo(hint, {});
-
requestedHint.belongsTo(team, {});
+
team.hasMany(requestedHint, {
"onDelete": "CASCADE",
"hooks": true
diff --git a/models/retosSuperados.js b/models/retosSuperados.js
new file mode 100644
index 00000000..3bee1399
--- /dev/null
+++ b/models/retosSuperados.js
@@ -0,0 +1,9 @@
+module.exports = function (sequelize, DataTypes) {
+ return sequelize.define(
+ "retosSuperados",
+ {
+ "success": DataTypes.BOOLEAN,
+ "answer": DataTypes.TEXT
+ }
+ );
+};
diff --git a/models/user.js b/models/user.js
index aa276d29..217c7a3b 100644
--- a/models/user.js
+++ b/models/user.js
@@ -21,9 +21,7 @@ module.exports = function (sequelize, DataTypes) {
},
"gender": {
"type": DataTypes.STRING,
- "validate": {
- "len": [0, 200]
- }
+ "validate": {"len": [0, 200]}
},
"username": {
"type": DataTypes.STRING,
diff --git a/public/js/play.js b/public/js/play.js
index bc24d280..ed3ed68e 100644
--- a/public/js/play.js
+++ b/public/js/play.js
@@ -271,6 +271,7 @@ const onHintResponse = async ({code, hintOrder: hintOrderPlus, puzzleOrder: puzz
};
const onInitialInfo = ({code, erState, participation}) => {
+ console.log(code, erState, participation)
if (participation != "AUTHOR" && (code && code === "NOK")) {
window.location = `/escapeRooms/${escapeRoomId}/`;
}
diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css
index a206fbde..e5275fb6 100644
--- a/public/stylesheets/style.css
+++ b/public/stylesheets/style.css
@@ -3267,7 +3267,8 @@ table.mainTable {
background-color: white;
border-radius: 6px;
border: 2px solid white;
- cursor: crosshair; }
+ cursor: crosshair;
+ transform: translateX(-6px); }
#mainSection > .main.timeline #timeline-chart .requestedHints .requestedHint.success {
background-color: #48C18E; }
#mainSection > .main.timeline #timeline-chart .requestedHints .requestedHint.fail {
@@ -3275,6 +3276,29 @@ table.mainTable {
#mainSection > .main.timeline #timeline-chart .requestedHints .requestedHint:hover .tooltip {
visibility: visible;
opacity: 1; }
+ #mainSection > .main.timeline #timeline-chart .retosNoSuperados {
+ position: absolute;
+ width: 100%;
+ display: inline-block;
+ height: 24px;
+ left: 0px;
+ bottom: 0px;
+ display: inline-block;
+ overflow: visible;
+ z-index: 99999;
+ bottom: 5px;
+ color: white; }
+ #mainSection > .main.timeline #timeline-chart .retosNoSuperados .failedPuzzle {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ cursor: crosshair;
+ transform: translateX(-6px); }
+ #mainSection > .main.timeline #timeline-chart .retosNoSuperados .failedPuzzle .material-icons {
+ font-size: 12px; }
+ #mainSection > .main.timeline #timeline-chart .retosNoSuperados .failedPuzzle:hover .tooltip {
+ visibility: visible;
+ opacity: 1; }
#mainSection > .main.timeline #legend {
display: flex;
flex-direction: row;
@@ -3336,17 +3360,25 @@ table.mainTable {
font-size: 12px;
width: auto;
min-width: 150px;
+ max-width: 600px;
padding: 10px;
line-height: 15px;
opacity: 0;
transition: 0.3s opacity ease-in;
z-index: 1;
text-align: left; }
+ #mainSection > .main.timeline .tooltip .tooltip-info {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap; }
#mainSection > .main.timeline .tooltip b {
font-weight: 700;
color: #161938; }
#mainSection > .main.timeline .tooltip * {
font-family: 'Lato'; }
+ #mainSection > .main.timeline .progress-puzzle-no .material-icons {
+ font-size: 12px;
+ vertical-align: initial; }
#mainSection > .main.timeline .progress-puzzle-0 {
background-color: var(--lightred); }
#mainSection > .main.timeline .progress-puzzle-0.step-flex {
diff --git a/public/stylesheets/timeline.sass b/public/stylesheets/timeline.sass
index 162a7e59..1d1764fa 100644
--- a/public/stylesheets/timeline.sass
+++ b/public/stylesheets/timeline.sass
@@ -129,6 +129,7 @@
border-radius: 6px
border: 2px solid white
cursor: crosshair
+ transform: translateX(-6px)
&.success
background-color: $brightgreen
&.fail
@@ -138,7 +139,30 @@
visibility: visible
opacity: 1
-
+ .retosNoSuperados
+ position: absolute
+ width: 100%
+ display: inline-block
+ height: 24px
+ left: 0px
+ bottom: 0px
+ display: inline-block
+ overflow: visible
+ z-index: 99999
+ bottom: 5px
+ color: white
+ .failedPuzzle
+ position: absolute
+ width: 12px
+ height: 12px
+ cursor: crosshair
+ transform: translateX(-6px)
+ .material-icons
+ font-size: 12px
+ &:hover
+ .tooltip
+ visibility: visible
+ opacity: 1
#legend
display: flex
flex-direction: row
@@ -202,18 +226,27 @@
font-size: 12px
width: auto
min-width: 150px
+ max-width: 600px
padding: 10px
line-height: 15px
opacity: 0
transition: 0.3s opacity ease-in
z-index: 1
text-align: left
+ .tooltip-info
+ overflow: hidden
+ text-overflow: ellipsis
+ white-space: nowrap
b
font-weight: 700
color: $darkerpurple
*
font-family: 'Lato'
+ .progress-puzzle-no
+ .material-icons
+ font-size: 12px
+ vertical-align: initial
.progress-puzzle-0
&.step-flex
background-image: linear-gradient(to right, var(--lightred) 80%, var(--mainpurple) 100%)
diff --git a/queries/team.js b/queries/team.js
index 554f1e16..0692aedd 100644
--- a/queries/team.js
+++ b/queries/team.js
@@ -1,6 +1,52 @@
const Sequelize = require("sequelize");
const {models} = require("../models");
+
+exports.teamRetosNoSuperados = (escapeRoomId, turnId) => {
+ const where = {
+ "attributes": ["id"],
+ "raw": true,
+ "where": {"startTime": {[Sequelize.Op.ne]: null}},
+ "include": [
+ {
+ "model": models.turno,
+ "attributes": [],
+ "where": {escapeRoomId}
+ },
+ {
+ "model": models.retosSuperados,
+ "as": "puzzlesSolved",
+ "attributes": ["puzzleId", "success", "createdAt", "answer"],
+ "where": {"success": false},
+ "required": false,
+ "duplicating": true,
+ "include": [
+ {
+ "model": models.puzzle,
+ "attributes": ["order"]
+ }
+ ]
+ }
+ ],
+ "order": [
+ Sequelize.literal("lower(team.name) ASC"),
+ [
+ {
+ "model": models.retosSuperados,
+ "as": "puzzlesSolved"
+ },
+ "createdAt",
+ "ASC"
+ ]
+ ]
+ };
+
+ if (turnId) {
+ where.include[0].where.id = turnId;
+ }
+ return where;
+};
+
exports.teamComplete = (escapeRoomId, turnId, order, waiting = false) => {
const where = {
// "attributes": [],
@@ -22,7 +68,10 @@ exports.teamComplete = (escapeRoomId, turnId, order, waiting = false) => {
{
"model": models.puzzle,
"as": "retos",
- "through": {"model": models.retosSuperados}
+ "through": {
+ "model": models.retosSuperados,
+ "where": {"success": true}
+ }
},
{
"model": models.requestedHint,
@@ -33,7 +82,6 @@ exports.teamComplete = (escapeRoomId, turnId, order, waiting = false) => {
"score",
"createdAt"
],
- // "where": {"success": true},
"required": false,
"include": [{"model": models.hint}]
}
@@ -91,7 +139,10 @@ exports.puzzlesByTeam = (escapeRoomId, turnId, hints = false) => {
{
"model": models.puzzle,
"as": "retos",
- "through": {"model": models.retosSuperados}
+ "through": {
+ "model": models.retosSuperados,
+ "where": {"success": true}
+ }
},
{
"model": models.user,
@@ -173,6 +224,7 @@ exports.ranking = (escapeRoomId, turnId) => {
"duplicating": true,
"through": {
"model": models.retosSuperados,
+ "where": {"success": true},
"attributes": ["createdAt"],
"required": true
}
@@ -249,6 +301,7 @@ exports.rankingShort = (escapeRoomId, turnId) => {
"duplicating": true,
"through": {
"model": models.retosSuperados,
+ "where": {"success": true},
"attributes": ["createdAt"],
"required": true
}
diff --git a/queries/user.js b/queries/user.js
index cf4d4ba3..22187c95 100644
--- a/queries/user.js
+++ b/queries/user.js
@@ -52,7 +52,10 @@ exports.puzzlesByParticipant = (escapeRoomId, turnId, orderBy, includeReqHints,
"model": models.puzzle,
// "attributes": ["id"],
"as": "retos",
- "through": {"model": models.retosSuperados}
+ "through": {
+ "model": models.retosSuperados,
+ "where": {"success": true}
+ }
}
]
diff --git a/seeders/20190208221150-FillEscapeRoomsTable.js b/seeders/20190208221150-FillEscapeRoomsTable.js
index 12a2c84a..e1f4ae2c 100755
--- a/seeders/20190208221150-FillEscapeRoomsTable.js
+++ b/seeders/20190208221150-FillEscapeRoomsTable.js
@@ -10,7 +10,7 @@ module.exports = {
"description": "Educational escape room",
"teamSize": 2,
"invitation": "assfdtWeQv",
- "teamInstructions": '[{"type":"text","puzzles":["0","all"],"payload":{"text":"You can add a custom message, images, links... Do not forget to setup when you want to display this information by clicking in the eye icon on the left.
\n"}}]',
+ "teamInstructions": "[{\"type\":\"text\",\"puzzles\":[\"0\",\"all\"],\"payload\":{\"text\":\"You can add a custom message, images, links... Do not forget to setup when you want to display this information by clicking in the eye icon on the left.\n\"}}]",
"authorId": 1,
"scoreParticipation": 40,
"createdAt": new Date(),
diff --git a/test/sockets.example.html b/test/sockets.example.html
index c44534b3..8ed7aef3 100644
--- a/test/sockets.example.html
+++ b/test/sockets.example.html
@@ -29,7 +29,6 @@
var node = document.createElement("LI");
node.appendChild(document.createTextNode(msgType));
document.getElementById("myList").appendChild(node);
- console.log({type: msgType, payload: msg})
}
//Initialize connection passing credentials (similar to Auth request in API REST)
diff --git a/views/escapeRooms/analytics/puzzleStats.ejs b/views/escapeRooms/analytics/puzzleStats.ejs
index 55709919..3b2c49e7 100644
--- a/views/escapeRooms/analytics/puzzleStats.ejs
+++ b/views/escapeRooms/analytics/puzzleStats.ejs
@@ -34,14 +34,14 @@
}
%>
-
+
<%=i18n.puzzle.Puzzle%> |
N |
<%=i18n.analytics.puzzleStats.cumulative%> |
<%=i18n.analytics.puzzleStats.nonCumulative%> |
-
+ Nº de intentos fallidos |
M |
@@ -54,6 +54,11 @@
SD |
MIN |
MAX |
+ M |
+ MED |
+ SD |
+ MIN |
+ MAX |
<% for (let p of escapeRoom.puzzles) {%>
@@ -69,6 +74,11 @@
<%if(summarySingle[p.id] && checkNaN(summarySingle[p.id].std)){%><%=secondsToDhms(summarySingle[p.id].std)%><%}else{%>-<%}%> |
<%if(summarySingle[p.id] && checkNaN(summarySingle[p.id].min)){%><%=secondsToDhms(summarySingle[p.id].min) || '0s'%><%}else{%>-<%}%> |
<%if(summarySingle[p.id] && checkNaN(summarySingle[p.id].max)){%><%=secondsToDhms(summarySingle[p.id].max)%><%}else{%>-<%}%> |
+ <%if(summaryNo[p.id] && checkNaN(summaryNo[p.id].mean)){%><%=(summaryNo[p.id].mean)%><%}else{%>-<%}%> |
+ <%if(summaryNo[p.id] && checkNaN(summaryNo[p.id].median)){%><%=(summaryNo[p.id].median)%><%}else{%>-<%}%> |
+ <%if(summaryNo[p.id] && checkNaN(summaryNo[p.id].std)){%><%=(summaryNo[p.id].std)%><%}else{%>-<%}%> |
+ <%if(summaryNo[p.id] && checkNaN(summaryNo[p.id].min)){%><%=(summaryNo[p.id].min) || '0s'%><%}else{%>-<%}%> |
+ <%if(summaryNo[p.id] && checkNaN(summaryNo[p.id].max)){%><%=(summaryNo[p.id].max)%><%}else{%>-<%}%> |
<%}%>
diff --git a/views/escapeRooms/analytics/timeline.ejs b/views/escapeRooms/analytics/timeline.ejs
index dbc37537..18088b7b 100644
--- a/views/escapeRooms/analytics/timeline.ejs
+++ b/views/escapeRooms/analytics/timeline.ejs
@@ -49,10 +49,14 @@
let puzzle = puzzles[p];
%>
-
- <%= puzzle.title %>
-
+
+ <%= puzzle.title %>
+
<% } %>
+
+
close
+ <%=i18n.analytics.timeline.failedToSolve%>
+
<%=i18n.analytics.timeline.closedDoor%>
@@ -66,12 +70,12 @@
- Hint obtained
+ <%=i18n.hint.hintObtained%>
<%if (escapeRoom.numQuestions){%>
- Hint failed to obtain
+ <%=i18n.hint.hintFailedToObtain%>
<%}%>
@@ -93,7 +97,7 @@
<%=i18n.analytics.timeline.legendMsg%>
-
+
<%
var calculatePuzzleTime = function(puzzleTime, prevPuzzleTime) {
return (puzzleTime - prevPuzzleTime)/60000;
@@ -105,6 +109,7 @@
}
var counter = teams.length;
var latest = Math.max(duration,Math.max(duration, ...teams.map(team => Math.max(duration,...team.retos.map(r=> (r.retosSuperados.createdAt-(team.turno.startTime || team.startTime))/60000)))))
+ let retosNoSuperadosAll = escapeRoom.retosNoSuperados;
%>
<% for (let t in teams) {
@@ -126,13 +131,14 @@
let now = new Date();
let end = new Date(startTime)
end.setTime(startTime.getTime() + (duration*60*1000));
+ let retosNoSuperadosTeam = retosNoSuperadosAll[team.id]
%>
-
<% for (let r in team.retos) {
let teamRetosLen = team.retos.length;
let reto = team.retos[r];
let puzzleTime = reto.retosSuperados.createdAt;
let perc = puzzlePosition(puzzleTime, prevPuzzleTime);
+
prevPuzzleTime = puzzleTime;
%>
@@ -201,9 +207,11 @@
<%=i18n.analytics.timeline.time%>: <%= Math.round(hintTime)%>'
-
- <%=i18n.analytics.timeline.score%>: <%= hint.score %>%
-
+ <%if(escapeRoom.numQuestions){%>
+
+ <%=i18n.analytics.timeline.score%>: <%= hint.score %>%
+
+ <% } %>
<%if(hint.hint){%>
<%=i18n.analytics.timeline.hint%>: <%= hint.hint.content %>
@@ -217,8 +225,32 @@
<% } %>
+
+ <% for (let rt in retosNoSuperadosTeam) { %>
+ <% if (rt !== null && rt != "null"){ %>
+ <% for (let rn of retosNoSuperadosTeam[rt]) { %>
+ <%
+ let rnTime = calculatePuzzleTime(rn.when, startTime);
+ %>
+
+ <% } %>
+ <% } %>
+ <% } %>
-
+
<% } %>
diff --git a/views/escapeRooms/showStudent.ejs b/views/escapeRooms/showStudent.ejs
index 6167ea6b..307a02c0 100644
--- a/views/escapeRooms/showStudent.ejs
+++ b/views/escapeRooms/showStudent.ejs
@@ -56,7 +56,9 @@
<% } else if(turno.status === "active" && !finished && (!team.startTime || (new Date(team.startTime.getTime() + escapeRoom.duration * 60000) > new Date()))) { %>
<%}%>