- TestMgr OO implementation
- Some styling
This commit is contained in:
parent
dbed288e2d
commit
ddd677c523
139
class/ExpressionBuilder.php
Normal file
139
class/ExpressionBuilder.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
class ExpressionBuilder
|
||||
{
|
||||
// Automatic typecast.
|
||||
static function automatic_typecast(string $rval) : mixed
|
||||
{
|
||||
if (is_numeric($rval)) { // is it a numeric value?
|
||||
if (((int)$rval) == ((double)$rval)) { // is it an integer?
|
||||
return (int)$rval;
|
||||
} else { // is it a float?
|
||||
return (double)$rval;
|
||||
}
|
||||
} else { // it's a string
|
||||
return substr($rval, 1, strlen($rval) - 2); // strip leading and trailing quotes
|
||||
}
|
||||
}
|
||||
|
||||
// Divide expression into operands and operators.
|
||||
static function splitCriterion(string $crstr): array
|
||||
{
|
||||
preg_match("/([<>=!]+|LIKE|NOT LIKE|IN|NOT IN|CONTAINS|NOT CONTAINS|BETWEEN|NOT BETWEEN|EXISTS)/", $crstr, $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
// extract operator
|
||||
$op = $matches[0][0];
|
||||
$op_pos = $matches[0][1];
|
||||
|
||||
// extract operands
|
||||
$left = trim(substr($crstr, 0, $op_pos));
|
||||
$right = trim(substr($crstr, $op_pos + strlen($op), strlen($crstr)));
|
||||
|
||||
// automatic type conversion
|
||||
if (str_starts_with($right, "[") && str_ends_with($right, "]")) { // is it an array?
|
||||
$right = substr($right, 1, -1); // strip leading and trailing brackets
|
||||
$elements = explode(",", $right); // extract array elements
|
||||
$right = []; // re-init right value, since it's an array
|
||||
foreach ($elements as $element) { // insert array elements
|
||||
$element = trim($element);
|
||||
if ($element !== "") {
|
||||
$right[] = automatic_typecast($element);
|
||||
}
|
||||
}
|
||||
} else { // it must be a single value
|
||||
$right = automatic_typecast($right);
|
||||
}
|
||||
|
||||
return [$left, $op, $right];
|
||||
}
|
||||
|
||||
// Build SleekDB query expression. Processes encapsulated expressions recursively as well.
|
||||
static function buildQuery(string $filter): array
|
||||
{
|
||||
// skip empty filter processing
|
||||
if (trim($filter) === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
// subfilters and operations
|
||||
$subfilts = [];
|
||||
$operations = [];
|
||||
|
||||
// buffer and scoring
|
||||
$k = 0;
|
||||
$k_prev = 0;
|
||||
$buffer = "";
|
||||
|
||||
for ($i = 0; $i < strlen($filter); $i++) {
|
||||
$c = $filter[$i];
|
||||
|
||||
// extract groups surrounded by parantheses
|
||||
if ($c === "(") {
|
||||
$k++;
|
||||
} elseif ($c === ")") {
|
||||
$k--;
|
||||
}
|
||||
|
||||
// only omit parentheses at the top-level expression
|
||||
if (!((($c === "(") && ($k === 1)) || (($c === ")") && ($k === 0)))) {
|
||||
$buffer .= $c;
|
||||
}
|
||||
|
||||
// if k = 0, then we found a subfilter
|
||||
if (($k === 0) && ($k_prev === 1)) {
|
||||
$subfilts[] = trim($buffer);
|
||||
$buffer = "";
|
||||
} elseif (($k === 1) && ($k_prev === 0)) {
|
||||
$op = trim($buffer);
|
||||
if ($op !== "") {
|
||||
$operations[] = $op;
|
||||
}
|
||||
$buffer = "";
|
||||
}
|
||||
|
||||
// save k to be used next iteration
|
||||
$k_prev = $k;
|
||||
}
|
||||
|
||||
// decide, whether further expansion of condition is needed
|
||||
$criteria = [];
|
||||
for ($i = 0; $i < count($subfilts); $i++) {
|
||||
$subfilt = $subfilts[$i];
|
||||
|
||||
// add subcriterion
|
||||
if ($subfilt[0] === "(") {
|
||||
$criteria[] = build_query($subfilt);
|
||||
} else {
|
||||
$criteria[] = split_criterion($subfilt);
|
||||
}
|
||||
|
||||
// add operator
|
||||
if (($i + 1) < count($subfilts)) {
|
||||
$criteria[] = $operations[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $criteria;
|
||||
}
|
||||
|
||||
// Build SleekDB ordering.
|
||||
static function buildOrdering(string $orderby): array
|
||||
{
|
||||
// don't process empty order instructions
|
||||
if ($orderby === "") {
|
||||
return [];
|
||||
}
|
||||
|
||||
// explode string at tokens delimiting separate order criteria
|
||||
$ordering = [];
|
||||
$subcriteria = explode(";", $orderby);
|
||||
foreach ($subcriteria as $subcriterion) {
|
||||
$parts = explode(":", $subcriterion); // fetch parts
|
||||
$field_name = trim($parts[0], "\ \n\r\t\v\0\"'"); // strip leading and trailing quotes if exists
|
||||
$direction = strtolower(trim($parts[1])); // fetch ordering direction
|
||||
$ordering[$field_name] = $direction; // build ordering instruction
|
||||
}
|
||||
|
||||
return $ordering;
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ class Game extends AutoStoring
|
||||
private string $publicId; // Public-accessible ID
|
||||
private int $VERSION; // Game representation version (used during updates)
|
||||
private GameMgr $gameMgr; // Game manager managing this instance
|
||||
private bool $challengesLoaded; // Indicates if challenges have been fetched
|
||||
private array $challenges; // Challenges
|
||||
|
||||
// -------
|
||||
@ -57,7 +58,7 @@ class Game extends AutoStoring
|
||||
// Load game challenges.
|
||||
public function loadChallenges(): void
|
||||
{
|
||||
if ($this->isGameFileIsPresent()) { // load if file is present
|
||||
if ($this->isGameFileIsPresent() && !$this->challengesLoaded) { // load if file is present
|
||||
$this->challenges = json_decode(file_get_contents($this->getGameFile()), true);
|
||||
}
|
||||
}
|
||||
@ -76,6 +77,8 @@ class Game extends AutoStoring
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->challengesLoaded = false;
|
||||
|
||||
$this->gameMgr = $gameMgr;
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
@ -326,6 +329,7 @@ class Game extends AutoStoring
|
||||
|
||||
public function getChallenges(): array
|
||||
{
|
||||
$this->loadChallenges();
|
||||
return $this->challenges;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,483 @@
|
||||
<?php
|
||||
|
||||
require_once "globals.php";
|
||||
require_once "AutoStoring.php";
|
||||
|
||||
require_once "ExpressionBuilder.php";
|
||||
|
||||
class TestSummary
|
||||
{
|
||||
public int $challengeN; // Number of challenges
|
||||
public int $correctAnswerN; // Number of correct answers
|
||||
|
||||
function __construct(int $challengeN, int $correctAnswerN)
|
||||
{
|
||||
$this->challengeN = $challengeN;
|
||||
$this->correctAnswerN = $correctAnswerN;
|
||||
}
|
||||
|
||||
// Get challenge count.
|
||||
function getChallengeN(): int
|
||||
{
|
||||
return $this->challengeN;
|
||||
}
|
||||
|
||||
// Get number of correct answers.
|
||||
function getCorrectAnswerN(): int
|
||||
{
|
||||
return $this->correctAnswerN;
|
||||
}
|
||||
|
||||
function setCorrectAnswerN(int $correctAnswerN): void
|
||||
{
|
||||
$this->correctAnswerN = $correctAnswerN;
|
||||
}
|
||||
|
||||
// Get ratio of correct results.
|
||||
function getPercentage(): float
|
||||
{
|
||||
return ($this->correctAnswerN * 100.0) / $this->challengeN;
|
||||
}
|
||||
|
||||
// Build from array.
|
||||
static function fromArray(array $a): TestSummary
|
||||
{
|
||||
return new TestSummary($a["challenge_n"], $a["correct_answer_n"]);
|
||||
}
|
||||
|
||||
// Convert to array.
|
||||
function toArray(): array
|
||||
{
|
||||
return ["challenge_n" => $this->challengeN, "correct_answer_n" => $this->correctAnswerN];
|
||||
}
|
||||
}
|
||||
|
||||
class Test
|
||||
class Test extends AutoStoring
|
||||
{
|
||||
public int $_id; // ID
|
||||
const TEST_ONGOING = "ongoing";
|
||||
const TEST_CONCLUDED = "concluded";
|
||||
|
||||
// ---------
|
||||
|
||||
public int $id; // ID
|
||||
public string $gameId; // ID of associated game
|
||||
public string $gameName; // Name of the associated game
|
||||
public string $nickname; // Associated user's nickname
|
||||
public string $state; // State of the test (ongoing/concluded)
|
||||
public bool $timeLimited; // The user is allowed to submit the test in a given period of time.
|
||||
public bool $repeatable; // Is the user allowed to take this test multiple times?
|
||||
public int $startTime; // Start time (UNIX timestamp)
|
||||
public int $endTime; // End time (UNIX timestamp)
|
||||
public int $endLimitTime; // Time limit on test submission (UNIX timestamp)
|
||||
public TestSummary $summary; // Summmary, if game has ended
|
||||
public array $challenges; // Test challenges
|
||||
|
||||
private TestMgr $testMgr; // Reference to TestMgr managing this Test instance
|
||||
|
||||
// -------------
|
||||
|
||||
// Preprocess challenges.
|
||||
private function preprocessChallenges(): void
|
||||
{
|
||||
foreach ($this->challenges as &$ch) {
|
||||
shuffle($ch["answers"]); // shuffle answers
|
||||
$ch["correct_answer"] = array_search($ch["correct_answer"], $ch["answers"]); // remap correct answer
|
||||
$ch["player_answer"] = -1; // create player answer field
|
||||
}
|
||||
}
|
||||
|
||||
// -------------
|
||||
|
||||
// Store modifications.
|
||||
public function storeMods(): void
|
||||
{
|
||||
$this->testMgr->updateTest($this);
|
||||
}
|
||||
|
||||
// -------------
|
||||
|
||||
// Construct new test based on Game and User objects
|
||||
function __construct(TestMgr &$testMgr, Game|array &$game_array, User &$user = null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->testMgr = $testMgr;
|
||||
$this->id = -1;
|
||||
|
||||
if (is_array($game_array)) { // populating fields from an array
|
||||
$a = &$game_array;
|
||||
|
||||
$this->id = $a["_id"] ?? -1;
|
||||
$this->gameId = $a["gameid"];
|
||||
$this->gameName = $a["gamename"];
|
||||
$this->nickname = $a["nickname"];
|
||||
$this->state = $a["state"];
|
||||
$this->timeLimited = $a["time_limited"];
|
||||
$this->startTime = $a["start_time"];
|
||||
$this->endTime = $a["end_time"] ?? 0;
|
||||
$this->endLimitTime = $a["end_limit_time"];
|
||||
$this->repeatable = $a["repeatable"];
|
||||
$this->challenges = $a["challenges"];
|
||||
if (isset($a["summary"])) {
|
||||
$this->summary = TestSummary::fromArray($a["summary"]);
|
||||
} else { // backward compatibility
|
||||
$this->summary = new TestSummary(count($a["challenges"]), 0);
|
||||
}
|
||||
} else { // populating fields from Game and User objects
|
||||
$game = &$game_array;
|
||||
|
||||
$this->endTime = 0;
|
||||
|
||||
// Fill-in basic properties
|
||||
$this->gameId = $game->getId();
|
||||
$this->gameName = $game->getName();
|
||||
$this->challenges = $game->getChallenges();
|
||||
$this->preprocessChallenges();
|
||||
$this->nickname = $user->getNickname();
|
||||
|
||||
$this->state = self::TEST_ONGOING;
|
||||
$gp = $game->getProperties();
|
||||
$this->timeLimited = (($gp["time_limit"] ?: -1) > -1);
|
||||
|
||||
$now = time();
|
||||
$this->startTime = $now;
|
||||
if ($this->timeLimited) {
|
||||
$this->endLimitTime = $now + $gp["time_limit"];
|
||||
}
|
||||
|
||||
$this->repeatable = $gp["repeatable"];
|
||||
|
||||
// Create a blank summary
|
||||
$this->summary = new TestSummary(count($this->challenges), 0);
|
||||
}
|
||||
|
||||
// auto-conclude time-constrained test if expired
|
||||
if ($this->isOngoing() && ($this->endLimitTime <= time())) {
|
||||
$this->concludeTest();
|
||||
}
|
||||
}
|
||||
|
||||
// Convert test to array.
|
||||
function toArray(array $omit = []): array
|
||||
{
|
||||
$a = [
|
||||
"_id" => $this->id,
|
||||
"gameid" => $this->gameId,
|
||||
"nickname" => $this->nickname,
|
||||
"gamename" => $this->gameName,
|
||||
"state" => $this->state,
|
||||
"time_limited" => $this->timeLimited,
|
||||
"start_time" => $this->startTime,
|
||||
"end_time" => $this->endTime,
|
||||
"end_limit_time" => $this->endLimitTime,
|
||||
"repeatable" => $this->repeatable,
|
||||
"challenges" => $this->challenges,
|
||||
"summary" => $this->summary->toArray()
|
||||
];
|
||||
|
||||
// omit specific fields
|
||||
foreach ($omit as $field) {
|
||||
unset($a[$field]);
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
// Get number of challenges.
|
||||
function getChallengeCount(): int
|
||||
{
|
||||
return count($this->challenges);
|
||||
}
|
||||
|
||||
// Save answer. Asserting $safe prevents saving answers to a concluded test.
|
||||
function saveAnswer(int $chidx, int $ansidx, bool $safe = true): bool
|
||||
{
|
||||
if (!$safe || $this->state === self::TEST_ONGOING) {
|
||||
if (($chidx < $this->getChallengeCount()) && ($ansidx < $this->challenges[$chidx]["answers"])) {
|
||||
$this->challenges[$chidx]["player_answer"] = $ansidx;
|
||||
$this->commitMods();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear answer.
|
||||
function clearAnswer(int $chidx, bool $safe = true): bool
|
||||
{
|
||||
if (!$safe || $this->state === self::TEST_ONGOING) {
|
||||
if ($chidx < $this->getChallengeCount()) {
|
||||
$this->challenges[$chidx]["player_answer"] = -1;
|
||||
$this->commitMods();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Conclude test.
|
||||
function concludeTest(): void
|
||||
{
|
||||
// check the answers
|
||||
$cans_n = 0; // number of correct answers
|
||||
foreach ($this->challenges as &$ch) {
|
||||
if ($ch["player_answer"] === $ch["correct_answer"]) {
|
||||
$cans_n++;
|
||||
}
|
||||
}
|
||||
|
||||
// set state and fill summary
|
||||
$this->state = TEST_CONCLUDED;
|
||||
$this->endTime = time();
|
||||
$this->summary->setCorrectAnswerN($cans_n);
|
||||
|
||||
// save test
|
||||
$this->commitMods();
|
||||
}
|
||||
|
||||
// --------
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStartTime(): int
|
||||
{
|
||||
return $this->startTime;
|
||||
}
|
||||
|
||||
public function getEndTime(): int
|
||||
{
|
||||
return $this->endTime;
|
||||
}
|
||||
|
||||
public function getSummary(): TestSummary
|
||||
{
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function getNickname(): string
|
||||
{
|
||||
return $this->nickname;
|
||||
}
|
||||
|
||||
public function getGameId(): string
|
||||
{
|
||||
return $this->gameId;
|
||||
}
|
||||
|
||||
public function isConcluded() : bool
|
||||
{
|
||||
return $this->state === self::TEST_CONCLUDED;
|
||||
}
|
||||
|
||||
public function isOngoing() : bool
|
||||
{
|
||||
return $this->state === self::TEST_ONGOING;
|
||||
}
|
||||
}
|
||||
|
||||
class TestMgr
|
||||
{
|
||||
private \SleekDB\Store $db; // test database
|
||||
|
||||
// -------------
|
||||
|
||||
// Update timed tests.
|
||||
// function updateTimedTests(array $test_data_array)
|
||||
// {
|
||||
// $now = time();
|
||||
// foreach ($test_data_array as $test_data) {
|
||||
// // look for unprocessed expired tests
|
||||
// if (($test_data["state"] === TEST_ONGOING) && ($test_data["time_limited"]) && ($test_data["end_limit_time"] < $now)) {
|
||||
// conclude_test($test_data["_id"]);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// -------------
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->db = new \SleekDB\Store(TESTDB, DATADIR, ["timeout" => false]);
|
||||
}
|
||||
|
||||
// Get test by ID.
|
||||
// Get test by ID from the database.
|
||||
function getTest(string $testid): Test|null
|
||||
{
|
||||
$test_data_array = $this->db->findById($testid);
|
||||
return count($test_data_array) != 0 ? new Test($this, $test_data_array) : null;
|
||||
}
|
||||
|
||||
// Update test in the database.
|
||||
function updateTest(Test &$test): void {
|
||||
$a = $test->toArray();
|
||||
$this->db->update($a);
|
||||
}
|
||||
|
||||
// Add test to the database.
|
||||
function addTest(Game &$game, User &$user): Test {
|
||||
// create new test
|
||||
$test = new Test($this,$game, $user);
|
||||
|
||||
// insert into database
|
||||
$a = $test->toArray(["_id"]);
|
||||
$a = $this->db->insert($a);
|
||||
|
||||
$test = new Test($this,$a);
|
||||
return $test;
|
||||
}
|
||||
|
||||
function addOrContinueTest(Game &$game, User &$user): Test|null {
|
||||
// check if the user had taken this test before
|
||||
$fetch_criteria = [["gameid", "=", (int)$game->getId()], "AND", ["nickname", "=", $user->getNickname()]];
|
||||
$previous_tests = $this->db->findBy($fetch_criteria);
|
||||
if (count($previous_tests) > 0) { // if there are previous attempts, then...
|
||||
fetch:
|
||||
// re-fetch tests, look only for ongoing
|
||||
$ongoing_tests = $this->db->findBy([$fetch_criteria, "AND", ["state", "=", TEST_ONGOING]]);
|
||||
if (count($ongoing_tests) !== 0) { // if there's an ongoing test
|
||||
$testid = $ongoing_tests[0]["_id"];
|
||||
$test = $this->getTest($testid);
|
||||
if ($test->isConcluded()) { // tests get concluded if they got found to be expired
|
||||
goto fetch; // like a loop...
|
||||
}
|
||||
return $test;
|
||||
} else { // there's no ongoing test
|
||||
if ($game->getProperties()["repeatable"]) { // test is repeatable...
|
||||
goto add_test;
|
||||
} else { // test is non-repeatable, cannot be attempted more times
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else { // there were no previous attempts
|
||||
goto add_test;
|
||||
}
|
||||
|
||||
// ----------------
|
||||
|
||||
add_test:
|
||||
return $this->addTest($game, $user);
|
||||
|
||||
}
|
||||
|
||||
// Delete test from the database.
|
||||
function deleteTest(string $testid): void {
|
||||
$this->db->deleteById($testid);
|
||||
}
|
||||
|
||||
// Get concluded tests by game ID and nickname.
|
||||
function getConcludedTests(string $gameid, string $nickname): array {
|
||||
$fetch_criteria = [["gameid", "=", (int)$gameid], "AND", ["nickname", "=", $nickname], "AND", ["state", "=", Test::TEST_CONCLUDED]];
|
||||
$test_data_array = $this->db->findBy($fetch_criteria);
|
||||
$tests = [];
|
||||
foreach ($test_data_array as $a) {
|
||||
$tests[] = new Test($this, $a);
|
||||
}
|
||||
return $tests;
|
||||
}
|
||||
|
||||
// Get test results by game ID.
|
||||
function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_challenge_data): array
|
||||
{
|
||||
$qb = $this->db->createQueryBuilder();
|
||||
$qb = $qb->where(["gameid", "=", (int)$gameid]);
|
||||
|
||||
// filtering
|
||||
if (trim($filter) !== "") {
|
||||
|
||||
// auto complete starting and ending parenthesis
|
||||
if (!str_starts_with($filter, "(")) {
|
||||
$filter = "(" . $filter;
|
||||
}
|
||||
if (!str_ends_with($filter, ")")) {
|
||||
$filter = $filter . ")";
|
||||
}
|
||||
|
||||
$criteria = ExpressionBuilder::buildQuery($filter);
|
||||
$qb->where($criteria);
|
||||
}
|
||||
|
||||
// ordering
|
||||
if (trim($orderby) !== "") {
|
||||
$ordering = ExpressionBuilder::buildOrdering($orderby);
|
||||
$qb->orderBy($ordering);
|
||||
}
|
||||
|
||||
// excluding challenge data
|
||||
if ($exclude_challenge_data) {
|
||||
$qb->except(["challenges"]);
|
||||
}
|
||||
|
||||
$test_data_array = $qb->getQuery()->fetch();
|
||||
return $test_data_array;
|
||||
}
|
||||
|
||||
// Generate detailed statistics.
|
||||
function generateDetailedStats(string $gameid, array $testids): array
|
||||
{
|
||||
if ((count($testids) === 0) || ($gameid === "")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// fetch relevant entries
|
||||
$qb = $this->db->createQueryBuilder();
|
||||
$criteria = [["gameid", "=", (int)$gameid], "AND", ["state", "=", "concluded"], "AND", ["_id", "IN", $testids]];
|
||||
$qb->where($criteria);
|
||||
$qb->select(["challenges"]);
|
||||
$entries = $qb->getQuery()->fetch();
|
||||
|
||||
$challenge_indices = [];
|
||||
|
||||
// count answers
|
||||
$aggregated = [];
|
||||
foreach ($entries as $entry) {
|
||||
foreach ($entry["challenges"] as $challenge) {
|
||||
$correct_answer = $challenge["answers"][$challenge["correct_answer"]];
|
||||
$compound = $challenge["question"] . $correct_answer . count($challenge["answers"]) . $challenge["image_url"];
|
||||
$idhash = md5($compound);
|
||||
|
||||
// if this is a new challenge to the list...
|
||||
if (!isset($challenge_indices[$idhash])) {
|
||||
$challenge_indices[$idhash] = count($challenge_indices);
|
||||
$challenge_info = [ // copy challenge info
|
||||
"hash" => $idhash,
|
||||
"image_url" => $challenge["image_url"],
|
||||
"question" => $challenge["question"],
|
||||
"answers" => $challenge["answers"],
|
||||
"correct_answer" => $correct_answer,
|
||||
"player_answers" => array_fill(0, count($challenge["answers"]), 0),
|
||||
"answer_count" => count($challenge["answers"]),
|
||||
];
|
||||
$aggregated[$challenge_indices[$idhash]] = $challenge_info; // insert challenge info
|
||||
}
|
||||
|
||||
// fetch challenge index
|
||||
$challenge_idx = $challenge_indices[$idhash];
|
||||
|
||||
// add up player answer
|
||||
$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]++;
|
||||
}
|
||||
}
|
||||
|
||||
// produce derived info
|
||||
foreach ($aggregated as &$entry) {
|
||||
$entry["answer_ratio"] = $entry["player_answers"];
|
||||
$answer_n = count($entry["answer_ratio"]);
|
||||
$sum = array_sum($entry["player_answers"]);
|
||||
|
||||
if ($sum === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $answer_n; $i++) {
|
||||
$entry["answer_ratio"][$i] = $entry["player_answers"][$i] / $sum;
|
||||
}
|
||||
}
|
||||
|
||||
// match challenges
|
||||
return $aggregated;
|
||||
}
|
||||
}
|
||||
108
interface.php
108
interface.php
@ -30,6 +30,8 @@ require_once "class/GroupMgr.php";
|
||||
|
||||
require_once "class/GameMgr.php";
|
||||
|
||||
require_once "class/TestMgr.php";
|
||||
|
||||
// ------------------------
|
||||
|
||||
$userMgr = new UserMgr();
|
||||
@ -93,6 +95,7 @@ $privilege = $user->getPrivilege();
|
||||
$is_quizmaster = $privilege === PRIVILEGE_QUIZMASTER;
|
||||
$groupMgr = new GroupMgr();
|
||||
$gameMgr = new GameMgr();
|
||||
$testMgr = new TestMgr();
|
||||
|
||||
/* ---------- ACTIONS REQUIRING BEING LOGGED IN ------------ */
|
||||
|
||||
@ -142,22 +145,28 @@ function get_available_games(ReqHandler &$rh, array $params): array
|
||||
|
||||
function start_or_continue_test(ReqHandler &$rh, array $params): string
|
||||
{
|
||||
global $nickname;
|
||||
$testid = create_or_continue_test($params["gameid"], $nickname);
|
||||
return $testid;
|
||||
global $user;
|
||||
global $gameMgr;
|
||||
global $testMgr;
|
||||
|
||||
$game = $gameMgr->getGame($params["gameid"]);
|
||||
$test = $testMgr->addOrContinueTest($game, $user);
|
||||
return $test->getId();
|
||||
}
|
||||
|
||||
function get_results_overview(ReqHandler &$rh, array $params): array
|
||||
{
|
||||
global $nickname;
|
||||
$concluded_tests = get_concluded_tests($params["gameid"], $nickname);
|
||||
global $user;
|
||||
global $testMgr;
|
||||
|
||||
$concluded_tests = $testMgr->getConcludedTests($params["gameid"], $user->getNickname());
|
||||
$overviews = [];
|
||||
foreach ($concluded_tests as $ct) {
|
||||
$overview = [
|
||||
"testid" => $ct["_id"],
|
||||
"start_time" => $ct["start_time"],
|
||||
"end_time" => $ct["end_time"],
|
||||
...$ct["summary"]
|
||||
"testid" => $ct->getId(),
|
||||
"start_time" => $ct->getStartTime(),
|
||||
"end_time" => $ct->getEndTime(),
|
||||
...($ct->getSummary()->toArray())
|
||||
];
|
||||
$overviews[] = $overview;
|
||||
}
|
||||
@ -184,49 +193,60 @@ $rh->add("get_results_overview", ["gameid"], PRIVILEGE_PLAYER, "get_results_over
|
||||
|
||||
|
||||
// test-related queries
|
||||
function does_test_belong_to_user(array $test_data): bool
|
||||
function does_test_belong_to_user(Test &$test): bool
|
||||
{
|
||||
global $nickname;
|
||||
return $test_data["nickname"] === $nickname;
|
||||
global $user;
|
||||
return $test->getNickname() === $user->getNickname();
|
||||
}
|
||||
|
||||
function is_test_access_approved(array $test_data): bool
|
||||
function is_test_access_approved(Test &$test): bool
|
||||
{
|
||||
global $nickname;
|
||||
global $is_quizmaster;
|
||||
global $user;
|
||||
global $gameMgr;
|
||||
|
||||
return does_test_belong_to_user($test_data) || is_user_contributor_to_game($test_data["gameid"], $nickname) || $is_quizmaster;
|
||||
$game = $gameMgr->getGame($test->getGameId());
|
||||
return does_test_belong_to_user($test) || $game->isUserContributorOrOwner($user->getNickname()) || $user->hasQuizmasterPrivilege();
|
||||
}
|
||||
|
||||
function access_test_data(string $testid): array|null
|
||||
function access_test_data(string $testid): Test|null
|
||||
{
|
||||
global $testMgr;
|
||||
|
||||
$testid = trim($testid);
|
||||
$test_data = ($testid !== "") ? get_test($testid) : null;
|
||||
$test = ($testid !== "") ? $testMgr->getTest($testid) : null;
|
||||
|
||||
// fetch test data
|
||||
if ($test_data === null) {
|
||||
return [];
|
||||
if ($test === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if access is approved to the specific test
|
||||
if (!is_test_access_approved($test_data)) {
|
||||
return [];
|
||||
if (!is_test_access_approved($test)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// update the test if timed
|
||||
update_timed_tests([$test_data]);
|
||||
// update_timed_tests([$test_data]); FIXME!!!
|
||||
|
||||
return $test_data;
|
||||
return $test;
|
||||
}
|
||||
|
||||
function exclude_correct_answers(array &$challenges) : void {
|
||||
foreach ($challenges as &$challenge) {
|
||||
$challenge["correct_answer"] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function get_player_test(ReqHandler &$rh, array $params): array
|
||||
{
|
||||
$result = [];
|
||||
$test_data = access_test_data($params["testid"]);
|
||||
$test = access_test_data($params["testid"]);
|
||||
|
||||
if ($test_data !== null) {
|
||||
$test_data_with_current_time = $test_data;
|
||||
if ($test !== null) {
|
||||
$test_data_with_current_time = $test->toArray();
|
||||
if ($test->isOngoing()) {
|
||||
exclude_correct_answers($test_data_with_current_time["challenges"]);
|
||||
}
|
||||
$test_data_with_current_time["current_time"] = time();
|
||||
$result = $test_data_with_current_time;
|
||||
}
|
||||
@ -236,8 +256,9 @@ function get_player_test(ReqHandler &$rh, array $params): array
|
||||
|
||||
function save_player_answer(ReqHandler &$rh, array $params): string
|
||||
{
|
||||
if (access_test_data($params["testid"] !== null)) {
|
||||
save_answer($params["testid"], $params["challenge_index"], $params["answer_index"]);
|
||||
$test = access_test_data($params["testid"]);
|
||||
if ($test !== null) {
|
||||
$test->saveAnswer($params["challenge_index"], $params["answer_index"]);
|
||||
return "OK";
|
||||
} else {
|
||||
return "FAIL";
|
||||
@ -246,8 +267,9 @@ function save_player_answer(ReqHandler &$rh, array $params): string
|
||||
|
||||
function submit_test(ReqHandler &$rh, array $params): string
|
||||
{
|
||||
if (access_test_data($params["testid"] !== null)) {
|
||||
conclude_test($params["testid"]);
|
||||
$test = access_test_data($params["testid"]);
|
||||
if ($test !== null) {
|
||||
$test->concludeTest();
|
||||
return "OK";
|
||||
} else {
|
||||
return "FAIL";
|
||||
@ -256,7 +278,8 @@ function submit_test(ReqHandler &$rh, array $params): string
|
||||
|
||||
function patch_through_image(string $gameid, string $img_url)
|
||||
{
|
||||
$game_dir = get_game_dir_by_gameid($gameid);
|
||||
global $gameMgr;
|
||||
$game_dir = $gameMgr->getGame($gameid)->getGameDir();
|
||||
$image_fetch_url = $game_dir . DIRECTORY_SEPARATOR . $img_url;
|
||||
|
||||
$img_fp = fopen($image_fetch_url, "r");
|
||||
@ -481,6 +504,7 @@ function export_game_file_csv(ReqHandler &$rh, array $params): string
|
||||
function get_player_results_by_gameid(ReqHandler &$rh, array $params): array
|
||||
{
|
||||
global $gameMgr;
|
||||
global $testMgr;
|
||||
global $user;
|
||||
|
||||
$gameid = trim($params["gameid"]);
|
||||
@ -491,7 +515,7 @@ function get_player_results_by_gameid(ReqHandler &$rh, array $params): array
|
||||
|
||||
$result = [];
|
||||
if (($game !== null) && ($game->isUserContributorOrOwner($user->getNickname()) || $user->hasQuizmasterPrivilege())) {
|
||||
$game_results = get_results_by_gameid($gameid, $filter, $ordering, true); // FIXME!!
|
||||
$game_results = $testMgr->getResultsByGameId($gameid, $filter, $ordering, true);
|
||||
$result = $game_results;
|
||||
}
|
||||
|
||||
@ -500,9 +524,11 @@ function get_player_results_by_gameid(ReqHandler &$rh, array $params): array
|
||||
|
||||
function generate_detailed_game_stats(ReqHandler &$rh, array $params): array
|
||||
{
|
||||
global $testMgr;
|
||||
|
||||
$testids = json_decode(trim($params["testids"]), true);
|
||||
$gameid = trim($params["gameid"]);
|
||||
$stats = generate_detailed_stats($gameid, $testids); // FIXME!!!
|
||||
$stats = $testMgr->generateDetailedStats($gameid, $testids);
|
||||
return $stats;
|
||||
}
|
||||
|
||||
@ -693,14 +719,14 @@ $rh->add("delete_users", ["users"], PRIVILEGE_QUIZMASTER, "delete_users", RESP_P
|
||||
$rh->add("get_all_users", [], PRIVILEGE_QUIZMASTER, "get_all_game_users", RESP_JSON, "Get all users.");
|
||||
$rh->add("import_users_from_csv", [], PRIVILEGE_QUIZMASTER, "import_users_from_csv", RESP_JSON, "Get all users.");
|
||||
|
||||
function test(ReqHandler &$rh, array $params): string
|
||||
{
|
||||
$usrmgr = new UserMgr();
|
||||
$nicknames = $usrmgr->getAllNicknames();
|
||||
return join(", ", $nicknames);
|
||||
}
|
||||
//function test(ReqHandler &$rh, array $params): string
|
||||
//{
|
||||
// $usrmgr = new UserMgr();
|
||||
// $nicknames = $usrmgr->getAllNicknames();
|
||||
// return join(", ", $nicknames);
|
||||
//}
|
||||
|
||||
$rh->add("test", [], PRIVILEGE_QUIZMASTER, "test", RESP_PLAIN, "Test.");
|
||||
//$rh->add("test", [], PRIVILEGE_QUIZMASTER, "test", RESP_PLAIN, "Test.");
|
||||
|
||||
// ----------
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ function populate_infobox(test_data, view_only) {
|
||||
time_left_s--;
|
||||
print_timer();
|
||||
if (time_left_s <= 0) {
|
||||
populate_all(test_data["_id"]);
|
||||
populate_all(test_data["_id"], test_data["gameid"]);
|
||||
clearInterval(INTERVAL_HANDLE);
|
||||
INTERVAL_HANDLE = null;
|
||||
}
|
||||
@ -183,6 +183,6 @@ function submit_test() {
|
||||
testid: TEST_DATA["_id"]
|
||||
}
|
||||
request(req).then(resp => {
|
||||
populate_all(TEST_DATA["_id"]);
|
||||
populate_all(TEST_DATA["_id"], TEST_DATA["gameid"]);
|
||||
});
|
||||
}
|
||||
@ -268,7 +268,9 @@ section.answer label {
|
||||
|
||||
section.answer label.correct-answer {
|
||||
border: 2px solid #176767 !important;
|
||||
padding: 0.1em;
|
||||
background-color: #176767;
|
||||
color: whitesmoke;
|
||||
/*padding: 0.1em;*/
|
||||
}
|
||||
|
||||
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
|
||||
@ -395,6 +397,10 @@ section.bad-answer {
|
||||
background-color: #e5d8d3;
|
||||
}
|
||||
|
||||
section.bad-answer section.answer input[type="radio"]:checked+label:not(.correct-answer) {
|
||||
background-color: #aa8a7d;
|
||||
}
|
||||
|
||||
section#further-info {
|
||||
font-size: 0.8em;
|
||||
padding: 0.4em 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user