Skip to content

Commit

Permalink
Add non-solved puzzles record
Browse files Browse the repository at this point in the history
  • Loading branch information
sonsoleslp committed May 4, 2021
1 parent 16cc7af commit 0a6dc82
Show file tree
Hide file tree
Showing 24 changed files with 386 additions and 55 deletions.
75 changes: 65 additions & 10 deletions controllers/analytics_controller.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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),
Expand All @@ -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);
Expand Down Expand Up @@ -516,15 +539,22 @@ 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;
const turno = user.turnosAgregados[0].startTime || user.teamsAgregados[0].startTime;
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);

Expand All @@ -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);
Expand All @@ -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 = {
Expand All @@ -577,27 +609,48 @@ 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",
"timestamp": convertDate(retoTS),
"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,
Expand All @@ -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
});
}
}
Expand Down
3 changes: 1 addition & 2 deletions controllers/escapeRoom_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down Expand Up @@ -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"]});

Expand Down
1 change: 1 addition & 0 deletions controllers/play_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
26 changes: 23 additions & 3 deletions helpers/analytics.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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;
Expand Down
57 changes: 46 additions & 11 deletions helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ exports.playInterface = async (name, req, res, next) => {
"through": {
"model": models.retosSuperados,
"required": false,
"where": {"success": true},
"attributes": ["createdAt"]
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}, {});
4 changes: 4 additions & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1053,6 +1055,8 @@
},
"hint": {
"Hints": "Hints",
"hintFailedToObtain": "Hint failed to obtain",
"hintObtained": "Hint obtained",
"attributes": {
"content": "The hint content",
"order": "The order",
Expand Down
6 changes: 5 additions & 1 deletion i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
},
"puzzleStats": {
"title": "Estadísticas de retos",
"cumulative": "Tiempos acumuladis",
"cumulative": "Tiempos acumulados",
"nonCumulative": "Tiempos no acumulados"
},
"puzzleTimes": {
Expand All @@ -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",
Expand Down Expand Up @@ -1042,6 +1044,8 @@
},
"hint": {
"Hints": "Pistas",
"hintFailedToObtain": "Pista no obtenida",
"hintObtained": "Pista obtenida",
"attributes": {
"content": "El contenido de la pista",
"order": "El orden",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

"use strict";

module.exports = {"up": (queryInterface) => queryInterface.removeConstraint("escapeRooms", "escapeRooms_invitation_key")};
Loading

0 comments on commit 0a6dc82

Please sign in to comment.