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 @@ } %>
-
+
- + @@ -54,6 +54,11 @@ + + + + + <% for (let p of escapeRoom.puzzles) {%> @@ -69,6 +74,11 @@ + + + + + <%}%>
<%=i18n.puzzle.Puzzle%> N <%=i18n.analytics.puzzleStats.cumulative%> <%=i18n.analytics.puzzleStats.nonCumulative%>Nº de intentos fallidos
MSD MIN MAXMMEDSDMINMAX
<%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%> <%=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); + %> +
+ close +
+
+ <%=puzzles[rt].title%> +
+
+ <%=i18n.analytics.timeline.failedToSolve%>: <%= Math.round(rnTime)%>' +
+
+ <%=i18n.analytics.timeline.providedAnswer%>: <%= rn.answer %> +
+
+
+ <% } %> + <% } %> + <% } %>
- +
<% } %> 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()))) { %>
- +
<%}%>