From 144c62f9adc64759690995112e729f51aed24184 Mon Sep 17 00:00:00 2001 From: Epagris Date: Tue, 1 Oct 2024 15:55:30 +0200 Subject: [PATCH] - LaTeX template improved: submission count, challenge fill and skip counts added - "List best results only" option added to each report generation functionality --- class/ReportBuilder.php | 41 ++++++++++++++-- class/TestMgr.php | 36 ++++++++++++++- game_manager_frame.php | 5 +- interface.php | 8 ++-- js/gamemgr.js | 9 ++-- js/result_analyzer.js | 4 +- report/template/report.tex | 95 ++++++++++++++++++++++++++++---------- result_analyzer.php | 3 +- 8 files changed, 160 insertions(+), 41 deletions(-) diff --git a/class/ReportBuilder.php b/class/ReportBuilder.php index 0a69ef6..94abbfd 100644 --- a/class/ReportBuilder.php +++ b/class/ReportBuilder.php @@ -7,7 +7,7 @@ require_once "common_func.php"; class ReportBuilder { - static public function getStatsByFilters(int $gameid, string $filter, string $groups, string $ordering) + static public function getStatsByFilters(int $gameid, string $filter, string $groups, bool $bestOnly) { $groupMgr = new GroupMgr(); $testMgr = new TestMgr(); @@ -18,7 +18,7 @@ class ReportBuilder $groupFilter = ["nickname", "IN", $nicknames]; // get IDs - $tests = $testMgr->getResultsByGameId($gameid, $filter, $ordering, true, $groupFilter); + $tests = $testMgr->getResultsByGameId($gameid, $filter, "", true, $bestOnly, $groupFilter); $ids = array_map(fn($test) => $test["_id"], $tests); // generate stats @@ -71,12 +71,25 @@ class Answer { return "\\answer" . $this->type . "{" . $this->ratio . "}{" . $this->text . "}\n"; } + + // Get answer ratio. + public function getRatio(): float + { + return $this->ratio; + } } class Challenge { private string $question; private array $answers; + private int $fillCount; + private int $skipCount; + + // Sort answers by ratio. + private function sortAnswers() : void { + usort($this->answers, function($a, $b) {return $a->getRatio() < $b->getRatio();}); + } function __construct(array $data) { @@ -87,6 +100,10 @@ class Challenge $type = $answer === $data["correct_answer"] ? Answer::CORRECT : Answer::INCORRECT; $this->answers[] = new Answer($type, $answer, $ratio); } + $this->fillCount = array_sum($data["player_answers"]); // get fill count + $this->skipCount = $data["skipped"]; + + $this->sortAnswers(); // sort answers by fill ratio } public function getQuestion(): string @@ -99,10 +116,22 @@ class Challenge return $this->answers; } + public function getFillCount(): int { + return $this->fillCount; + } + + public function getSkipCount(): int { + return $this->skipCount; + } + + public function getSubmissionCount() : int { + return $this->fillCount + $this->skipCount; + } + // Generate TeX representation. public function genTeX(): string { - $tex = "\\begin{question}{" . $this->question . "}\n"; + $tex = "\\begin{question}{" . $this->question . "}{" . $this->fillCount . "}\n"; foreach ($this->answers as &$answer) { $tex .= $answer->genTeX(); } @@ -116,6 +145,10 @@ class ReportSection private string $title; private array $challenges; + private function getNumberOfSubmissions() : int { + return count($this->challenges) > 0 ? $this->challenges[0]->getSubmissionCount() : 0; + } + function __construct(string $title, array $challenges) { $this->title = $title; @@ -135,7 +168,7 @@ class ReportSection // Generate TeX representation of this report. function genTeX(): string { - $tex = "\\begin{quiz}{" . $this->title . "}\n"; + $tex = "\\begin{quiz}{" . $this->title . "}{" . $this->getMaxFillingCount() . "}\n"; foreach ($this->challenges as $challenge) { $tex .= $challenge->genTeX(); } diff --git a/class/TestMgr.php b/class/TestMgr.php index 46dce6e..629fe03 100644 --- a/class/TestMgr.php +++ b/class/TestMgr.php @@ -406,7 +406,7 @@ class TestMgr } // Get test results by game ID. - function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_challenge_data, array ...$furtherFilters): array + function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_challenge_data, bool $best_ones_only, array ...$furtherFilters): array { $qb = $this->db->createQueryBuilder(); $qb = $qb->where(["gameid", "=", (int)$gameid]); @@ -448,6 +448,34 @@ class TestMgr } $test_data_array = $qb->getQuery()->fetch(); + + // if only the best results should be included, then... + if ($best_ones_only) { + // filter out ongoing ones + $tests = array_filter($test_data_array, fn($test) => $test["state"] === Test::TEST_CONCLUDED); + + // sort by result + usort($tests, fn($a, $b) => $a["summary"]["percentage"] > $b["summary"]["percentage"]); + + // gather best tests by username here + $best_test_ids = []; + foreach ($tests as $test) { + $nickname = $test["nickname"]; + if (!in_array($nickname, $best_test_ids)) { + $best_tests[$nickname] = $test["_id"]; + } + } + + // just keep values, drop the keys (nicknames) + $best_tests = array_values($best_tests); + + // remove non-best results + $test_data_array = array_filter($test_data_array, fn($test) => in_array($test["_id"], $best_tests)); + + // renumber results + $test_data_array = array_values($test_data_array); + } + return $test_data_array; } @@ -487,6 +515,7 @@ class TestMgr "correct_answer" => $correct_answer, "player_answers" => array_fill(0, count($challenge["answers"]), 0), "answer_count" => count($challenge["answers"]), + "skipped" => 0 ]; $aggregated[$challenge_indices[$idhash]] = $challenge_info; // insert challenge info } @@ -495,9 +524,12 @@ class TestMgr $challenge_idx = $challenge_indices[$idhash]; // add up player answer - if (trim($challenge["player_answer"]) !== "") { + $player_answer = trim($challenge["player_answer"]); + if (($player_answer !== "") && ($player_answer != -1)) { // player answered $answer_idx = array_search($challenge["answers"][$challenge["player_answer"]], $aggregated[$challenge_idx]["answers"]); // transform player answer index to report answer index $aggregated[$challenge_idx]["player_answers"][(int)$answer_idx]++; + } else { // player has not answered or provided an unprocessable answer + $aggregated[$challenge_idx]["skipped"]++; } } } diff --git a/game_manager_frame.php b/game_manager_frame.php index 1faeef3..3cd948d 100644 --- a/game_manager_frame.php +++ b/game_manager_frame.php @@ -146,7 +146,10 @@ if (!get_autologin_state() || (($user_data["privilege"] !== PRIVILEGE_CREATOR) & +
+ +
+
diff --git a/interface.php b/interface.php index 5d76a84..024a0e7 100644 --- a/interface.php +++ b/interface.php @@ -485,6 +485,7 @@ function get_results_by_gameid(ReqHandler &$rh, array $params): array $filter = trim($params["filter"] ?? ""); $ordering = trim($params["orderby"] ?? ""); $groups = explode_list(trim($params["groups"] ?? "")); + $best_only = trim($params["best_only"] ?? "false") === "true"; $game = $gameMgr->getGame($gameid); @@ -516,9 +517,9 @@ function get_results_by_gameid(ReqHandler &$rh, array $params): array // execute filtering $game_results = null; if ($group_filter !== []) { - $game_results = $testMgr->getResultsByGameId($gameid, $filter, $ordering, true, $group_filter); + $game_results = $testMgr->getResultsByGameId($gameid, $filter, $ordering, true, $best_only, $group_filter); } else { - $game_results = $testMgr->getResultsByGameId($gameid, $filter, $ordering, true); + $game_results = $testMgr->getResultsByGameId($gameid, $filter, $ordering, true, $best_only); } $result = $game_results; } @@ -534,6 +535,7 @@ function generate_report_by_groups(ReqHandler &$rh, array $params): string $gameid = trim($params["gameid"]); $filter = trim($params["filter"] ?? ""); $groups = explode_list(trim($params["groups"])); + $best_only = trim($params["best_only"] ?? "false") === "true"; $outtype = trim($params["outtype"] ?? "pdf"); // only PDF and TEX are valid @@ -565,7 +567,7 @@ function generate_report_by_groups(ReqHandler &$rh, array $params): string // assemble report $report = new Report($game->getName()); foreach ($groups as $groupname) { - $stats = ReportBuilder::getStatsByFilters($gameid, $filter, $groupname, ""); + $stats = ReportBuilder::getStatsByFilters($gameid, $filter, $groupname, $best_only); $section = new ReportSection($groupname, $stats); $report->addSection($section); } diff --git a/js/gamemgr.js b/js/gamemgr.js index 1404720..e864cb0 100644 --- a/js/gamemgr.js +++ b/js/gamemgr.js @@ -373,12 +373,13 @@ function list_results_by_game(game) { function generate_game_report_by_groups() { let gameid = EDITED_GAME["_id"]; - let filter = document.getElementById("report_filter").value.trim() - let groups = document.getElementById("report_groups").value.trim() + let filter = document.getElementById("report_filter").value.trim(); + let groups = document.getElementById("report_groups").value.trim(); + let best_only = document.getElementById("best_only").checked ? "true" : "false"; let outtype = document.getElementById("report_output_type").value; - let report_url = `interface.php?action=generate_report_by_groups&gameid=${gameid}&groups=${groups}&filter=${filter}&outtype=${outtype}`; - report_url = report_url.replace("#", "%23"); + let report_url = `interface.php?action=generate_report_by_groups&gameid=${gameid}&groups=${groups}&filter=${filter}&outtype=${outtype}&best_only=${best_only}`; + report_url = report_url.replaceAll("#", "%23"); window.open(report_url, "_blank"); } diff --git a/js/result_analyzer.js b/js/result_analyzer.js index 58ac09b..9f7136c 100644 --- a/js/result_analyzer.js +++ b/js/result_analyzer.js @@ -36,6 +36,7 @@ function fetch_results() { let filterF = document.getElementById("filter"); let orderbyF = document.getElementById("orderby"); let groupsF = document.getElementById("groups"); + let best_onlyChk = document.getElementById("best_only"); let filter = autoconvert_datetime(filterF.value.trim()); @@ -46,7 +47,8 @@ function fetch_results() { gameid: GAMEID, filter: filter.trim(), orderby: orderbyF.value.trim(), - groups: groupsF.value.trim() + groups: groupsF.value.trim(), + best_only: best_onlyChk.checked ? "true" : "false" }; request(req).then(resp => { diff --git a/report/template/report.tex b/report/template/report.tex index bef3bad..471d090 100644 --- a/report/template/report.tex +++ b/report/template/report.tex @@ -1,12 +1,17 @@ +% !TeX spellcheck = hu_HU +% !TeX encoding = UTF-8 +% !TeX program = xelatex + \documentclass[10pt]{article} %% Importing packages % General things \usepackage[magyar]{babel} -\usepackage[a4paper,margin=1in]{geometry} +\usepackage[a4paper,margin=10mm,footskip=5mm]{geometry} \usepackage{fontspec} \usepackage[fontsize=7pt]{fontsize} +\usepackage{titlesec} % For frame layout and content \usepackage{xcolor} @@ -28,6 +33,10 @@ \definecolor{colpbb}{HTML}{A0A0A0} % Progressbar background \definecolor{colanc}{HTML}{176767} % Correct answer background \definecolor{colani}{HTML}{D3E5E5} % Incorrect answer background +\definecolor{colsbg}{HTML}{AA8A7D} % Group title background +\definecolor{colsfg}{HTML}{EFEFEF} % Group title text color +\definecolor{colqsb}{HTML}{176767} % Quiz sequence number background +\definecolor{colqsf}{HTML}{EFEFEF} % Quiz sequence number text color % Setting up outer frames \mdfsetup{ @@ -52,16 +61,16 @@ % Replacing ToC text \addto\captionsmagyar{% - \renewcommand{\contentsname}% - {\huge Kurzusok}% + \renewcommand{\contentsname}% + {\huge Csoportok}% } % Add dotfill to ToC \makeatletter \patchcmd{\l@section}{\hfil}{% - \leaders\hbox{$\m@th + \leaders\hbox{$\m@th \mkern \@dotsep mu\hbox{.}\mkern \@dotsep - mu$}\hfill}{}{} + mu$}\hfill}{}{} \makeatother @@ -83,39 +92,75 @@ \newcommand{\unnumsec}[1]{\section*{#1}% \addcontentsline{toc}{section}{#1}} +% Set section title style +\titleformat{\unnumsec}{\Large\bfseries}{\thesection}{1em}{} + % Macros for answers \newcommand{\answerCorrect}[2]{\progressbar{#1} & \begin{minipage}{0.8\columnwidth}\begin{mdframed}[backgroundcolor=colanc]\color{coltxl}{#2}\end{mdframed}\end{minipage}\\} \newcommand{\answerIncorrect}[2]{\progressbar[filledcolor=colpbs]{#1} & \begin{minipage}{0.8\columnwidth}\begin{mdframed}[backgroundcolor=colani]\color{coltxd}{#2}\end{mdframed}\end{minipage}\\} +% Macros for questions. +\newmdenv[backgroundcolor=colqsb,align=left,innerleftmargin=0.2em,innerrightmargin=0.2em]{qsn} % Question sequence number + % Environment for questions. The parameter is the question itself, the contents of the environment should consist of \answer*{}{} macros and nothing else -\newenvironment{question}[1]{ -\begin{center} - \begin{mdframed} - {\color{coltit}\large #1\par}\medskip - \begin{tabular}{c c} -}{ - \end{tabular} - \end{mdframed} -\end{center} +\newenvironment{question}[2]{ + \begin{center} + \begin{mdframed} + \raisebox{0.05em} { + \begin{minipage}{0.6cm} + \begin{qsn}[userdefinedwidth=0.5cm] + \begin{flushright} + \color{colqsf}\arabic{qc}. + \end{flushright} + \end{qsn} + \end{minipage} + } + % + {\color{coltit}\large #1\par}\medskip + % + {\hspace{1em}\small #2~kitöltő}\vspace{0.6em}\par + + \stepcounter{qc} + + \begin{tabular}{c c} + }{ + \end{tabular} + + \end{mdframed} + \end{center} } % An environment to create a quiz containing questions. The parameter is either the title of the quiz, or the name of the course -\newenvironment{quiz}[1]{ - \unnumsec{#1} -}{ +\newenvironment{quiz}[2]{ +% Create highlighted group title + \begin{mdframed}[backgroundcolor=colsbg] + { + \color{colsfg} + \unnumsec{#1} + {\small #2~kitöltő} + \vspace{0.4em} + } + \end{mdframed} + + % Reset question counter + \setcounter{qc}{1} + }{ \clearpage } \begin{document} -\begin{center} - \Huge \IfFileExists{stats/title.tex}{\input{title.tex}}{Kvíz eredmények} -\end{center} + \begin{center} + \Huge \IfFileExists{title.tex}{\input{title.tex}}{Kvíz eredmények} + \end{center} -\Large -\tableofcontents -\normalsize -\clearpage + \Large + \tableofcontents + \normalsize + \clearpage -\inputIfFileExists{content.tex} + % Create question counter + \newcounter{qc} + + \inputIfFileExists{content.tex} \end{document} diff --git a/result_analyzer.php b/result_analyzer.php index d0b208c..bd71f03 100644 --- a/result_analyzer.php +++ b/result_analyzer.php @@ -52,7 +52,8 @@ if (!$gameMgr->getGame($game_id)->isUserContributorOrOwner($user_data["nickname" - +
+