diff --git a/class/ExpressionBuilder.php b/class/ExpressionBuilder.php new file mode 100644 index 0000000..2ceb7db --- /dev/null +++ b/class/ExpressionBuilder.php @@ -0,0 +1,139 @@ +=!]+|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; + } +} \ No newline at end of file diff --git a/class/GameMgr.php b/class/GameMgr.php index 507a875..efd5d19 100644 --- a/class/GameMgr.php +++ b/class/GameMgr.php @@ -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; } } diff --git a/class/TestMgr.php b/class/TestMgr.php index 9374719..2758edc 100644 --- a/class/TestMgr.php +++ b/class/TestMgr.php @@ -1,39 +1,483 @@ 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; + } } \ No newline at end of file diff --git a/interface.php b/interface.php index fc88ce0..f62e5de 100644 --- a/interface.php +++ b/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."); // ---------- diff --git a/js/testground.js b/js/testground.js index a76a6fc..b739422 100644 --- a/js/testground.js +++ b/js/testground.js @@ -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"]); }); } \ No newline at end of file diff --git a/style/spreadquiz.css b/style/spreadquiz.css index 727cd86..cc3598a 100644 --- a/style/spreadquiz.css +++ b/style/spreadquiz.css @@ -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;