- ...
This commit is contained in:
parent
7c533f91f0
commit
1fa2924abd
515
class/Game.php
Normal file
515
class/Game.php
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "vendor/autoload.php";
|
||||||
|
|
||||||
|
require_once "AutoStoring.php";
|
||||||
|
|
||||||
|
class Game extends AutoStoring
|
||||||
|
{
|
||||||
|
public const DEFAULT_GAME_PROPERTIES = [
|
||||||
|
"forward_only" => false, // player may traverse back and forth between tasks
|
||||||
|
"time_limit" => 0, // no time limit; otherwise, this field indicates time limit in seconds
|
||||||
|
"repeatable" => false // this test can be taken multiple times
|
||||||
|
];
|
||||||
|
|
||||||
|
public const CURRENT_GAME_VERSION = 2; // MUST BE INCREMENTED!!
|
||||||
|
|
||||||
|
// --------
|
||||||
|
private int $id; // Game's ID
|
||||||
|
private string $name; // Game's name
|
||||||
|
private string $owner; // Game's owner
|
||||||
|
private array $contributors; // Contributors to the game
|
||||||
|
private string $description; // Game's description
|
||||||
|
private bool $gameFileIsPresent; // Indicates if game CSV is in place
|
||||||
|
private array $properties; // Collection of several game properties
|
||||||
|
private bool $public; // Is this game publicly available?
|
||||||
|
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 $tasksLoaded; // Indicates if tasks have been fetched
|
||||||
|
private array $tasks; // Tasks
|
||||||
|
|
||||||
|
// -------
|
||||||
|
|
||||||
|
static public function genPublicId(): string
|
||||||
|
{
|
||||||
|
return uniqid("p");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------
|
||||||
|
|
||||||
|
static private function patchUpGameData(array &$a): void
|
||||||
|
{
|
||||||
|
$version = $a["version"] ?? 0;
|
||||||
|
if ($version < 2) { // update to game version 2
|
||||||
|
if (!key_exists("public_id", $a)) {
|
||||||
|
$a["public"] = false;
|
||||||
|
$a["public_id"] = self::genPublicId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$a["version"] = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store modifications.
|
||||||
|
public function storeMods(): void
|
||||||
|
{
|
||||||
|
$this->gameMgr->updateGame($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit modifications.
|
||||||
|
function commitMods(): void
|
||||||
|
{
|
||||||
|
//$this->patchUpGameDate();
|
||||||
|
parent::commitMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load game tasks.
|
||||||
|
public function loadTasks(): void
|
||||||
|
{
|
||||||
|
if ($this->isGameFileIsPresent() && !$this->tasksLoaded) { // load if file is present
|
||||||
|
$this->tasks = TaskFactory::constructFromCollection(json_decode(file_get_contents($this->getGameFile()), true));
|
||||||
|
foreach ($this->tasks as &$ch) {
|
||||||
|
$ch->setTemplate(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save tasks.
|
||||||
|
public function saveTasks(): void
|
||||||
|
{
|
||||||
|
file_put_contents($this->getGameFile(), json_encode($this->tasks)); // store tasks in JSON-format
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------
|
||||||
|
|
||||||
|
function __construct(GameMgr &$gameMgr, string $name, string $description = "", int $id = -1, string $owner = "",
|
||||||
|
array $contributors = [], bool $gameFileIsPresent = false, array $properties = [],
|
||||||
|
bool $public = false, string $publicId = "", int $version = 2)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->tasksLoaded = false;
|
||||||
|
|
||||||
|
$this->gameMgr = $gameMgr;
|
||||||
|
$this->id = $id;
|
||||||
|
$this->name = $name;
|
||||||
|
$this->description = $description;
|
||||||
|
$this->owner = $owner;
|
||||||
|
$this->contributors = $contributors;
|
||||||
|
$this->gameFileIsPresent = $gameFileIsPresent;
|
||||||
|
$this->properties = $properties;
|
||||||
|
$this->public = $public;
|
||||||
|
$this->publicId = $publicId;
|
||||||
|
$this->VERSION = $version;
|
||||||
|
$this->tasks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create game from array representation.
|
||||||
|
static function fromArray(GameMgr &$gameMgr, array $a): Game
|
||||||
|
{
|
||||||
|
$id = $a["_id"] ?? -1;
|
||||||
|
self::patchUpGameData($a);
|
||||||
|
return new Game($gameMgr, $a["name"], $a["description"], $id, $a["owner"], $a["contributors"],
|
||||||
|
$a["game_file_present"], $a["properties"], $a["public"], $a["public_id"], $a["version"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const OMIT_ADVANCED_FIELDS = ["contributors", "game_file_is_present", "properties", "public", "public_id", "version"];
|
||||||
|
|
||||||
|
// Convert game to array representation.
|
||||||
|
function toArray(array $omit = []): array
|
||||||
|
{
|
||||||
|
$a = [
|
||||||
|
"_id" => $this->id,
|
||||||
|
"name" => $this->name,
|
||||||
|
"description" => $this->description,
|
||||||
|
"owner" => $this->owner,
|
||||||
|
"contributors" => $this->contributors,
|
||||||
|
"game_file_present" => $this->gameFileIsPresent,
|
||||||
|
"properties" => $this->properties,
|
||||||
|
"public" => $this->public,
|
||||||
|
"public_id" => $this->publicId,
|
||||||
|
"version" => $this->VERSION,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($omit as $field) {
|
||||||
|
unset($a[$field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export tasks to a CSV file. TODO: ez csak a feleletválasztóshoz lesz jó
|
||||||
|
function exportTasksToCSV(&$f): void
|
||||||
|
{
|
||||||
|
// load tasks
|
||||||
|
$this->loadTasks();
|
||||||
|
|
||||||
|
// populate CSV file
|
||||||
|
foreach ($this->tasks as $ch) {
|
||||||
|
if ($ch->getType() === "singlechoice") {
|
||||||
|
$csvline = [
|
||||||
|
$ch["question"],
|
||||||
|
$ch["image_url"],
|
||||||
|
];
|
||||||
|
$csvline = array_merge($csvline, $ch["answers"]);
|
||||||
|
fputcsv($f, $csvline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get game directory NAME with path. Does not check if the game directory exists or not.
|
||||||
|
function getGameDir(): string
|
||||||
|
{
|
||||||
|
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get game file NAME with path. Does not check whether the game file is in place or not.
|
||||||
|
function getGameFile(): string
|
||||||
|
{
|
||||||
|
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId() . DIRECTORY_SEPARATOR . GAME_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the given user the owner of the game?
|
||||||
|
function isUserOwner(string $nickname): bool
|
||||||
|
{
|
||||||
|
return $this->owner === $nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is the given user a contributor of the game?
|
||||||
|
function isUserContributor(string $nickname): bool
|
||||||
|
{
|
||||||
|
return in_array($nickname, $this->contributors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is user contributor or owner?
|
||||||
|
function isUserContributorOrOwner(string $nickname): bool
|
||||||
|
{
|
||||||
|
return $this->isUserContributor($nickname) || $this->isUserOwner($nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CSV_ENCODINGS = ["UTF-8", "Windows-1252"];
|
||||||
|
|
||||||
|
// Import tasks from a CSV table. TODO: ez csak a feleletválasztós betöltésére lesz jó
|
||||||
|
function importTasksFromCSV(string $csv_path): array
|
||||||
|
{
|
||||||
|
// convert text encoding into UTF-8
|
||||||
|
$data = file_get_contents($csv_path);
|
||||||
|
$encoding = "UNKNOWN";
|
||||||
|
foreach (self::CSV_ENCODINGS as $enc) { // detect encoding
|
||||||
|
if (mb_check_encoding($data, $enc)) {
|
||||||
|
$encoding = $enc;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($encoding !== "UNKNOWN") { // if encoding has been detected successfully
|
||||||
|
$data = mb_convert_encoding($data, "UTF-8", $encoding);
|
||||||
|
file_put_contents($csv_path, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear tasks
|
||||||
|
$this->tasks = [];
|
||||||
|
|
||||||
|
// load filled CSV file
|
||||||
|
$f = fopen($csv_path, "r");
|
||||||
|
if (!$f) { // failed to open file
|
||||||
|
return ["n" => 0, "encoding" => $encoding];
|
||||||
|
}
|
||||||
|
while ($csvline = fgetcsv($f)) {
|
||||||
|
// skip empty lines
|
||||||
|
if (trim(implode("", $csvline)) === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (count($csvline) >= 3) {
|
||||||
|
// construct task record
|
||||||
|
$a = [
|
||||||
|
"question" => trim($csvline[0]),
|
||||||
|
"image_url" => trim($csvline[1]),
|
||||||
|
"correct_answer" => 0,
|
||||||
|
"answers" => array_filter(array_slice($csvline, 2), function ($v) {
|
||||||
|
return trim($v) !== "";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// if an image is attached to the task, then give a random name to the image
|
||||||
|
if ($a["image_url"] !== "") {
|
||||||
|
$a["image_url"] = $this->obfuscateAttachedImage($a["image_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the task
|
||||||
|
$this->tasks[] = new SingleChoiceTask($a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose($f);
|
||||||
|
|
||||||
|
// save tasks
|
||||||
|
$this->saveTasks();
|
||||||
|
|
||||||
|
// update game with game file present
|
||||||
|
$this->gameFileIsPresent = true;
|
||||||
|
|
||||||
|
// store modifications
|
||||||
|
$this->commitMods();
|
||||||
|
|
||||||
|
return ["n" => count($this->tasks), "encoding" => $encoding];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getTableVersion(string &$cA1): string
|
||||||
|
{
|
||||||
|
if (str_starts_with($cA1, "#:V")) {
|
||||||
|
return trim(substr($cA1, 3));
|
||||||
|
} else {
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function obfuscateAttachedImage(string $old_img_name): string
|
||||||
|
{
|
||||||
|
$ext = pathinfo($old_img_name, PATHINFO_EXTENSION);
|
||||||
|
$ext = ($ext !== "") ? ("." . $ext) : $ext;
|
||||||
|
$new_img_name = uniqid("img_", true) . $ext;
|
||||||
|
|
||||||
|
// rename the actual file
|
||||||
|
$old_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $old_img_name;
|
||||||
|
$new_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $new_img_name;
|
||||||
|
rename($old_img_path, $new_img_path);
|
||||||
|
|
||||||
|
return $new_img_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function importTasksFromTableV1(array &$table): array
|
||||||
|
{
|
||||||
|
$n = count($table);
|
||||||
|
for ($i = 1; $i < $n; $i++) {
|
||||||
|
$row = &$table[$i]; // fetch row
|
||||||
|
$a = [ // create initializing array
|
||||||
|
"question" => trim($row[0]),
|
||||||
|
"image_url" => trim($row[1]),
|
||||||
|
"correct_answer" => 0,
|
||||||
|
"answers" => array_filter(array_slice($row, 2), function ($v) {
|
||||||
|
return trim($v ?? "") !== "";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
// obfuscate image filename
|
||||||
|
if ($a["image_url"] !== "") {
|
||||||
|
$a["image_url"] = $this->obfuscateAttachedImage($a["image_url"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the task
|
||||||
|
$this->tasks[] = new SingleChoiceTask($a);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["n" => $n, "encoding" => "automatikusan konvertált"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getTableColumnIndices(array &$header): array
|
||||||
|
{
|
||||||
|
$columns = [];
|
||||||
|
for ($i = 1; $i < count($header); $i++) { // skip the first column as it is metadata
|
||||||
|
$label = $header[$i];
|
||||||
|
if (($label ?? "") !== "") {
|
||||||
|
$columns[$label] = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getFirstUnlabeledColumn(array &$header): int
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < count($header); $i++) {
|
||||||
|
if (trim($header[$i] ?? "") === "") {
|
||||||
|
return $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function explodeFlags(string $fs): array {
|
||||||
|
$flags = explode(",", trim($fs));
|
||||||
|
return array_filter($flags, fn($v) => trim($v) !== "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function importTasksFromTableV2(array &$table): array
|
||||||
|
{
|
||||||
|
$result = ["n" => 0, "encoding" => "automatikusan konvertált"]; // prepare result
|
||||||
|
$n = count($table); // get number of entries (including header)
|
||||||
|
if ($n === 0) { // cannot import an empty table
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$header = &$table[0]; // extract header
|
||||||
|
|
||||||
|
$fuc = Game::getFirstUnlabeledColumn($header); // get first unlabeled column
|
||||||
|
if ($fuc === -1) { // if there's no data, then it is impossible to create the tasks
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = Game::getTableColumnIndices($header); // fetch column names
|
||||||
|
|
||||||
|
// start iterating over tasks
|
||||||
|
for ($i = 1; $i < $n; $i++) {
|
||||||
|
$row = &$table[$i]; // fetch row
|
||||||
|
|
||||||
|
// prepare a function that looks up the fields referenced by their labels
|
||||||
|
$select_fn = function (array $cols) use (&$row, &$columns) {
|
||||||
|
for ($i = 0; $i < count($cols); $i++) {
|
||||||
|
if (isset($columns[$cols[$i]])) {
|
||||||
|
return trim($row[$columns[$cols[$i]]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
// prepare a function that extracts all unlabeled fields
|
||||||
|
$extract_unlabeled_fn = fn() => array_filter(array_slice($row, $fuc), function ($v) {
|
||||||
|
return trim($v ?? "") !== "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch generic fields
|
||||||
|
$a = [
|
||||||
|
"flags" => Game::explodeFlags($row[0] ?? ""),
|
||||||
|
"type" => strtolower($select_fn(["Típus", "Type"])),
|
||||||
|
"generator" => $select_fn(["Generátor", "Generator"]),
|
||||||
|
"image_url" => $select_fn(["Kép", "Image"]),
|
||||||
|
"question" => $select_fn(["Kérdés", "Question"]),
|
||||||
|
];
|
||||||
|
|
||||||
|
// convert into
|
||||||
|
switch ($a["type"]) {
|
||||||
|
case "singlechoice":
|
||||||
|
$a["answers"] = $extract_unlabeled_fn();
|
||||||
|
$a["correct_answer"] = 0;
|
||||||
|
break;
|
||||||
|
case "openended":
|
||||||
|
$a["correct_answers"] = $extract_unlabeled_fn();
|
||||||
|
break;
|
||||||
|
case "numberconversion":
|
||||||
|
$a["instruction"] = $row[$fuc];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the task
|
||||||
|
$this->tasks[] = TaskFactory::fromArray($a);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result["n"] = $n - 1;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function importTasksFromTable(array &$table): array
|
||||||
|
{
|
||||||
|
// clear tasks
|
||||||
|
$this->tasks = [];
|
||||||
|
|
||||||
|
// get table version
|
||||||
|
$vs = Game::getTableVersion($table[0][0]);
|
||||||
|
|
||||||
|
// continue processing based on table version
|
||||||
|
$result = ["n" => 0, "encoding" => "ismeretlen"];
|
||||||
|
switch ($vs) {
|
||||||
|
case "1":
|
||||||
|
$result = $this->importTasksFromTableV1($table);
|
||||||
|
break;
|
||||||
|
case "2":
|
||||||
|
$result = $this->importTasksFromTableV2($table);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the number of imported tasks is not zero, then it was a successful import
|
||||||
|
$this->gameFileIsPresent = false; // assume no game file present
|
||||||
|
if ($result["n"] > 0) {
|
||||||
|
$this->saveTasks(); // save tasks
|
||||||
|
$this->gameFileIsPresent = true; // update game with game file present
|
||||||
|
$this->commitMods(); // store modifications
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): void
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOwner(): string
|
||||||
|
{
|
||||||
|
return $this->owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOwner(string $owner): void
|
||||||
|
{
|
||||||
|
$this->owner = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContributors(): array
|
||||||
|
{
|
||||||
|
return $this->contributors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContributors(array $contributors): void
|
||||||
|
{
|
||||||
|
$this->contributors = $contributors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(string $description): void
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isGameFileIsPresent(): bool
|
||||||
|
{
|
||||||
|
return $this->gameFileIsPresent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProperties(array $properties): void
|
||||||
|
{
|
||||||
|
$this->properties = $properties;
|
||||||
|
$this->commitMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function& getProperties(): array
|
||||||
|
{
|
||||||
|
return $this->properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPublic(): bool
|
||||||
|
{
|
||||||
|
return $this->public;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPublicId(): string
|
||||||
|
{
|
||||||
|
return $this->publicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPublic(bool $public): void
|
||||||
|
{
|
||||||
|
$this->public = $public;
|
||||||
|
$this->commitMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTasks(): array
|
||||||
|
{
|
||||||
|
$this->loadTasks();
|
||||||
|
return $this->tasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,351 +4,7 @@ require_once "vendor/autoload.php";
|
|||||||
|
|
||||||
require_once "AutoStoring.php";
|
require_once "AutoStoring.php";
|
||||||
|
|
||||||
class Game extends AutoStoring
|
require_once "Game.php";
|
||||||
{
|
|
||||||
public const DEFAULT_GAME_PROPERTIES = [
|
|
||||||
"forward_only" => false, // player may traverse back and forth between challenges
|
|
||||||
"time_limit" => 0, // no time limit; otherwise, this field indicates time limit in seconds
|
|
||||||
"repeatable" => false // this test can be taken multiple times
|
|
||||||
];
|
|
||||||
|
|
||||||
public const CURRENT_GAME_VERSION = 2; // MUST BE INCREMENTED!!
|
|
||||||
|
|
||||||
// --------
|
|
||||||
private int $id; // Game's ID
|
|
||||||
private string $name; // Game's name
|
|
||||||
private string $owner; // Game's owner
|
|
||||||
private array $contributors; // Contributors to the game
|
|
||||||
private string $description; // Game's description
|
|
||||||
private bool $gameFileIsPresent; // Indicates if game CSV is in place
|
|
||||||
private array $properties; // Collection of several game properties
|
|
||||||
private bool $public; // Is this game publicly available?
|
|
||||||
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
|
|
||||||
|
|
||||||
// -------
|
|
||||||
|
|
||||||
static public function genPublicId(): string
|
|
||||||
{
|
|
||||||
return uniqid("p");
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------
|
|
||||||
|
|
||||||
static private function patchUpGameData(array &$a) : void
|
|
||||||
{
|
|
||||||
$version = $a["version"] ?? 0;
|
|
||||||
if ($version < 2) { // update to game version 2
|
|
||||||
if (!key_exists("public_id", $a)) {
|
|
||||||
$a["public"] = false;
|
|
||||||
$a["public_id"] = self::genPublicId();
|
|
||||||
}
|
|
||||||
|
|
||||||
$a["version"] = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($version < 3) {
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
//$a["version"] = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store modifications.
|
|
||||||
public function storeMods() : void
|
|
||||||
{
|
|
||||||
$this->gameMgr->updateGame($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit modifications.
|
|
||||||
function commitMods(): void
|
|
||||||
{
|
|
||||||
//$this->patchUpGameDate();
|
|
||||||
parent::commitMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load game challenges.
|
|
||||||
public function loadChallenges(): void
|
|
||||||
{
|
|
||||||
if ($this->isGameFileIsPresent() && !$this->challengesLoaded) { // load if file is present
|
|
||||||
$this->challenges = json_decode(file_get_contents($this->getGameFile()), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save challenges.
|
|
||||||
public function saveChallenges(): void
|
|
||||||
{
|
|
||||||
file_put_contents($this->getGameFile(), json_encode($this->challenges)); // store challenges in JSON-format
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------
|
|
||||||
|
|
||||||
function __construct(GameMgr &$gameMgr, string $name, string $description = "", int $id = -1, string $owner = "",
|
|
||||||
array $contributors = [], bool $gameFileIsPresent = false, array $properties = [],
|
|
||||||
bool $public = false, string $publicId = "", int $version = 2)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
|
|
||||||
$this->challengesLoaded = false;
|
|
||||||
|
|
||||||
$this->gameMgr = $gameMgr;
|
|
||||||
$this->id = $id;
|
|
||||||
$this->name = $name;
|
|
||||||
$this->description = $description;
|
|
||||||
$this->owner = $owner;
|
|
||||||
$this->contributors = $contributors;
|
|
||||||
$this->gameFileIsPresent = $gameFileIsPresent;
|
|
||||||
$this->properties = $properties;
|
|
||||||
$this->public = $public;
|
|
||||||
$this->publicId = $publicId;
|
|
||||||
$this->VERSION = $version;
|
|
||||||
$this->challenges = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create game from array representation.
|
|
||||||
static function fromArray(GameMgr &$gameMgr, array $a): Game
|
|
||||||
{
|
|
||||||
$id = $a["_id"] ?? -1;
|
|
||||||
self::patchUpGameData($a);
|
|
||||||
return new Game($gameMgr, $a["name"], $a["description"], $id, $a["owner"], $a["contributors"],
|
|
||||||
$a["game_file_present"], $a["properties"], $a["public"], $a["public_id"], $a["version"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const OMIT_ADVANCED_FIELDS = ["contributors", "game_file_is_present", "properties", "public", "public_id", "version"];
|
|
||||||
|
|
||||||
// Convert game to array representation.
|
|
||||||
function toArray(array $omit = []): array
|
|
||||||
{
|
|
||||||
$a = [
|
|
||||||
"_id" => $this->id,
|
|
||||||
"name" => $this->name,
|
|
||||||
"description" => $this->description,
|
|
||||||
"owner" => $this->owner,
|
|
||||||
"contributors" => $this->contributors,
|
|
||||||
"game_file_present" => $this->gameFileIsPresent,
|
|
||||||
"properties" => $this->properties,
|
|
||||||
"public" => $this->public,
|
|
||||||
"public_id" => $this->publicId,
|
|
||||||
"version" => $this->VERSION,
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($omit as $field) {
|
|
||||||
unset($a[$field]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export challenges to a CSV file. TODO: ez csak a feleletválasztóshoz lesz jó
|
|
||||||
function exportChallengesToCSV(&$f): void
|
|
||||||
{
|
|
||||||
// load challenges
|
|
||||||
$this->loadChallenges();
|
|
||||||
|
|
||||||
// populate CSV file
|
|
||||||
foreach ($this->challenges as $ch) {
|
|
||||||
$csvline = [
|
|
||||||
$ch["question"],
|
|
||||||
$ch["image_url"],
|
|
||||||
];
|
|
||||||
$csvline = array_merge($csvline, $ch["answers"]);
|
|
||||||
fputcsv($f, $csvline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get game directory NAME with path. Does not check if the game directory exists or not.
|
|
||||||
function getGameDir(): string
|
|
||||||
{
|
|
||||||
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get game file NAME with path. Does not check whether the game file is in place or not.
|
|
||||||
function getGameFile(): string
|
|
||||||
{
|
|
||||||
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId() . DIRECTORY_SEPARATOR . GAME_FILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is the given user the owner of the game?
|
|
||||||
function isUserOwner(string $nickname): bool
|
|
||||||
{
|
|
||||||
return $this->owner === $nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is the given user a contributor of the game?
|
|
||||||
function isUserContributor(string $nickname): bool
|
|
||||||
{
|
|
||||||
return in_array($nickname, $this->contributors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is user contributor or owner?
|
|
||||||
function isUserContributorOrOwner(string $nickname): bool
|
|
||||||
{
|
|
||||||
return $this->isUserContributor($nickname) || $this->isUserOwner($nickname);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CSV_ENCODINGS = ["UTF-8", "Windows-1252"];
|
|
||||||
|
|
||||||
// Import challenges from a CSV table. TODO: ez csak a feleletválasztós betöltésére lesz jó
|
|
||||||
function importChallengesFromCSV(string $csv_path): array
|
|
||||||
{
|
|
||||||
// convert text encoding into UTF-8
|
|
||||||
$data = file_get_contents($csv_path);
|
|
||||||
$encoding = "UNKNOWN";
|
|
||||||
foreach (self::CSV_ENCODINGS as $enc) { // detect encoding
|
|
||||||
if (mb_check_encoding($data, $enc)) {
|
|
||||||
$encoding = $enc;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($encoding !== "UNKNOWN") { // if encoding has been detected successfully
|
|
||||||
$data = mb_convert_encoding($data, "UTF-8", $encoding);
|
|
||||||
file_put_contents($csv_path, $data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear challenges
|
|
||||||
$this->challenges = [];
|
|
||||||
|
|
||||||
// load filled CSV file
|
|
||||||
$f = fopen($csv_path, "r");
|
|
||||||
if (!$f) { // failed to open file
|
|
||||||
return ["n" => 0, "encoding" => $encoding];
|
|
||||||
}
|
|
||||||
while ($csvline = fgetcsv($f)) {
|
|
||||||
// skip empty lines
|
|
||||||
if (trim(implode("", $csvline)) === "") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (count($csvline) >= 3) {
|
|
||||||
// construct challenge record
|
|
||||||
$ch = [
|
|
||||||
"question" => trim($csvline[0]),
|
|
||||||
"image_url" => trim($csvline[1]),
|
|
||||||
"correct_answer" => trim($csvline[2]),
|
|
||||||
"answers" => array_filter(array_slice($csvline, 2), function ($v) {
|
|
||||||
return trim($v) !== "";
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
// if image is attached to the challenge, then give a random name to the image
|
|
||||||
if ($ch["image_url"] !== "") {
|
|
||||||
$old_img_name = $ch["image_url"];
|
|
||||||
$ext = pathinfo($old_img_name, PATHINFO_EXTENSION);
|
|
||||||
$ext = ($ext !== "") ? ("." . $ext) : $ext;
|
|
||||||
$new_img_name = uniqid("img_", true) . $ext;
|
|
||||||
$ch["image_url"] = $new_img_name;
|
|
||||||
|
|
||||||
// rename the actual file
|
|
||||||
$old_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $old_img_name;
|
|
||||||
$new_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $new_img_name;
|
|
||||||
rename($old_img_path, $new_img_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the challenge
|
|
||||||
$this->challenges[] = $ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fclose($f);
|
|
||||||
|
|
||||||
// save challenges
|
|
||||||
$this->saveChallenges();
|
|
||||||
|
|
||||||
// update game with game file present
|
|
||||||
$this->gameFileIsPresent = true;
|
|
||||||
|
|
||||||
// store modifications
|
|
||||||
$this->commitMods();
|
|
||||||
|
|
||||||
return ["n" => count($this->challenges), "encoding" => $encoding];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------
|
|
||||||
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setName(string $name): void
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getOwner(): string
|
|
||||||
{
|
|
||||||
return $this->owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setOwner(string $owner): void
|
|
||||||
{
|
|
||||||
$this->owner = $owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getContributors(): array
|
|
||||||
{
|
|
||||||
return $this->contributors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setContributors(array $contributors): void
|
|
||||||
{
|
|
||||||
$this->contributors = $contributors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return $this->description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setDescription(string $description): void
|
|
||||||
{
|
|
||||||
$this->description = $description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getId(): int
|
|
||||||
{
|
|
||||||
return $this->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isGameFileIsPresent(): bool
|
|
||||||
{
|
|
||||||
return $this->gameFileIsPresent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProperties(array $properties): void {
|
|
||||||
$this->properties = $properties;
|
|
||||||
$this->commitMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function& getProperties(): array
|
|
||||||
{
|
|
||||||
return $this->properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isPublic(): bool
|
|
||||||
{
|
|
||||||
return $this->public;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPublicId(): string
|
|
||||||
{
|
|
||||||
return $this->publicId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setPublic(bool $public) : void {
|
|
||||||
$this->public = $public;
|
|
||||||
$this->commitMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getChallenges(): array
|
|
||||||
{
|
|
||||||
$this->loadChallenges();
|
|
||||||
return $this->challenges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GameMgr
|
class GameMgr
|
||||||
{
|
{
|
||||||
@ -383,7 +39,7 @@ class GameMgr
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addGame(string $name, string $owner, string $description, array $properties = Game::DEFAULT_GAME_PROPERTIES,
|
function addGame(string $name, string $owner, string $description, array $properties = Game::DEFAULT_GAME_PROPERTIES,
|
||||||
array $contributors = [], array $challenges = []): bool
|
array $contributors = [], array $tasks = []): bool
|
||||||
{
|
{
|
||||||
$game_data = [
|
$game_data = [
|
||||||
"name" => $name,
|
"name" => $name,
|
||||||
@ -403,7 +59,7 @@ class GameMgr
|
|||||||
$game = Game::fromArray($this, $game_data);
|
$game = Game::fromArray($this, $game_data);
|
||||||
$current_game_media_dir = $game->getGameDir();
|
$current_game_media_dir = $game->getGameDir();
|
||||||
mkdir($current_game_media_dir);
|
mkdir($current_game_media_dir);
|
||||||
$game->saveChallenges();
|
$game->saveTasks();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,5 +122,22 @@ class GameMgr
|
|||||||
$gameIds = array_map(fn($r) => $r["name"] . "#" . $r["_id"] ,$a);
|
$gameIds = array_map(fn($r) => $r["name"] . "#" . $r["_id"] ,$a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function upgradeGames(array $ids = []): void
|
||||||
|
{
|
||||||
|
$a = [];
|
||||||
|
if ($ids === []) {
|
||||||
|
$a = $this->db->findAll();
|
||||||
|
} else {
|
||||||
|
$a = $this->db->findBy(["_id", "IN", $ids]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($a as $g) {
|
||||||
|
$game = Game::fromArray($this, $g);
|
||||||
|
$game->loadTasks();
|
||||||
|
$game->saveTasks();
|
||||||
|
$game->storeMods();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------
|
// -------
|
||||||
}
|
}
|
||||||
|
|||||||
226
class/Group.php
Normal file
226
class/Group.php
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "AutoStoring.php";
|
||||||
|
|
||||||
|
class Group extends AutoStoring
|
||||||
|
{
|
||||||
|
private int $_id; // Group's ID (assigned by SleekDB)
|
||||||
|
private string $name; // Group's name
|
||||||
|
private bool $unique; // Indicates if name is unique or not
|
||||||
|
private string $owner; // Group owner's nickname
|
||||||
|
private string $description; // Group description
|
||||||
|
private array $editors; // Nicknames of users able to manage the group
|
||||||
|
private array $members; // Nickname of group members
|
||||||
|
private array $games; // Game IDs assigned to this group
|
||||||
|
private GroupMgr $groupMgr; // Reference to GroupMgr object managing this group
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
|
||||||
|
// store modifications to the database
|
||||||
|
public function storeMods() : void
|
||||||
|
{
|
||||||
|
$this->groupMgr->updateGroup($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------
|
||||||
|
|
||||||
|
function __construct(GroupMgr &$groupMgr, string $name, string $description, string $owner, int $id = -1, bool $unique = true, array $editors = [], array $members = [], array $games = [])
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->_id = $id;
|
||||||
|
$this->name = $name;
|
||||||
|
$this->unique = $unique;
|
||||||
|
$this->description = $description;
|
||||||
|
$this->owner = $owner;
|
||||||
|
$this->editors = $editors;
|
||||||
|
$this->members = $members;
|
||||||
|
$this->games = $games;
|
||||||
|
$this->groupMgr = &$groupMgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Group from array
|
||||||
|
static function fromArray(GroupMgr &$groupMgr, array $a): Group
|
||||||
|
{
|
||||||
|
$id = $a["_id"] ?? -1;
|
||||||
|
return new Group($groupMgr, $a["groupname"], $a["description"], $a["owner"], $id, $a["unique"], $a["editors"], $a["users"], $a["games"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Group to array
|
||||||
|
function toArray(array $omit = []): array
|
||||||
|
{
|
||||||
|
$a = [
|
||||||
|
"_id" => $this->_id,
|
||||||
|
"groupname" => $this->name,
|
||||||
|
"unique" => $this->unique,
|
||||||
|
"description" => $this->description,
|
||||||
|
"owner" => $this->owner,
|
||||||
|
"editors" => $this->editors,
|
||||||
|
"users" => $this->members,
|
||||||
|
"games" => $this->games
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($omit as $field) {
|
||||||
|
unset($a[$field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get group's ID.
|
||||||
|
function getID(): int
|
||||||
|
{
|
||||||
|
return $this->_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get group's name.
|
||||||
|
function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set group's name.
|
||||||
|
function setName(string $name): void
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->storeMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell if group is unique
|
||||||
|
function isUnique(): bool
|
||||||
|
{
|
||||||
|
return $this->unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get group's description.
|
||||||
|
function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set group's description.
|
||||||
|
function setDescription(string $description): void
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
$this->storeMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get group's owner.
|
||||||
|
function getOwner(): string
|
||||||
|
{
|
||||||
|
return $this->owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set group's owner.
|
||||||
|
function setOwner(string $owner): void
|
||||||
|
{
|
||||||
|
$this->owner = $owner;
|
||||||
|
$this->storeMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of editors.
|
||||||
|
function getEditors(): array
|
||||||
|
{
|
||||||
|
return $this->editors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set editors.
|
||||||
|
function setEditors(array $editors): void
|
||||||
|
{
|
||||||
|
$this->editors = $editors;
|
||||||
|
$this->storeMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get group members.
|
||||||
|
function getMembers(): array
|
||||||
|
{
|
||||||
|
return $this->members;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set group members.
|
||||||
|
function setMembers(array $members): void
|
||||||
|
{
|
||||||
|
$this->members = $members;
|
||||||
|
$this->storeMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get games.
|
||||||
|
function getGames(): array
|
||||||
|
{
|
||||||
|
return $this->games;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set games.
|
||||||
|
function setGames(array $games): void
|
||||||
|
{
|
||||||
|
$this->games = $games;
|
||||||
|
$this->storeMods();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include/exclude members.
|
||||||
|
function changeMembers(array $nicknames_add, array $nicknames_remove): void
|
||||||
|
{
|
||||||
|
foreach ($nicknames_add as $nickname) { // add members
|
||||||
|
alter_array_contents($this->members, $nickname, null);
|
||||||
|
}
|
||||||
|
foreach ($nicknames_remove as $nickname) { // remove members
|
||||||
|
alter_array_contents($this->members, null, $nickname); // delete from members
|
||||||
|
alter_array_contents($this->editors, null, $nickname); // delete from editors
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storeMods(); // store changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add members
|
||||||
|
function addMembers(array $nicknames) : void {
|
||||||
|
$this->changeMembers($nicknames, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove members
|
||||||
|
function removeMembers(array $nicknames) : void {
|
||||||
|
$this->changeMembers([], $nicknames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include/exclude games.
|
||||||
|
function changeGames(array $gameids_add, array $gameids_remove): void
|
||||||
|
{
|
||||||
|
foreach ($gameids_add as $gameid) { // add games
|
||||||
|
alter_array_contents($this->games, $gameid, null);
|
||||||
|
}
|
||||||
|
foreach ($gameids_remove as $gameid) { // remove games
|
||||||
|
alter_array_contents($this->games, null, $gameid);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->storeMods(); // store changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether the user is an editor of this group.
|
||||||
|
function isUserEditor(string $nickname): bool
|
||||||
|
{
|
||||||
|
return in_array($nickname, $this->editors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether the user is an editor or the owner of the group.
|
||||||
|
function isUserContributor(string $nickname): bool
|
||||||
|
{
|
||||||
|
return $this->isUserEditor($nickname) || ($this->owner === $nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns if user is member of the group.
|
||||||
|
function isMember(string $nickname): bool
|
||||||
|
{
|
||||||
|
return in_array($nickname, $this->members);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if game is assigned to this group.
|
||||||
|
function isGameAssigned(string $gameid): bool
|
||||||
|
{
|
||||||
|
return in_array($gameid, $this->games);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get groups unique name.
|
||||||
|
function getUniqueName(): string
|
||||||
|
{
|
||||||
|
return $this->name . ($this->unique ? "" : ("#" . $this->_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,228 +6,7 @@ require_once "AutoStoring.php";
|
|||||||
|
|
||||||
require_once "privilege_levels.php";
|
require_once "privilege_levels.php";
|
||||||
|
|
||||||
class Group extends AutoStoring
|
require_once "Group.php";
|
||||||
{
|
|
||||||
private int $_id; // Group's ID (assigned by SleekDB)
|
|
||||||
private string $name; // Group's name
|
|
||||||
private bool $unique; // Indicates if name is unique or not
|
|
||||||
private string $owner; // Group owner's nickname
|
|
||||||
private string $description; // Group description
|
|
||||||
private array $editors; // Nicknames of users able to manage the group
|
|
||||||
private array $members; // Nickname of group members
|
|
||||||
private array $games; // Game IDs assigned to this group
|
|
||||||
private GroupMgr $groupMgr; // Reference to GroupMgr object managing this group
|
|
||||||
|
|
||||||
// --------------
|
|
||||||
|
|
||||||
// store modifications to the database
|
|
||||||
public function storeMods() : void
|
|
||||||
{
|
|
||||||
$this->groupMgr->updateGroup($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------
|
|
||||||
|
|
||||||
function __construct(GroupMgr &$groupMgr, string $name, string $description, string $owner, int $id = -1, bool $unique = true, array $editors = [], array $members = [], array $games = [])
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
|
|
||||||
$this->_id = $id;
|
|
||||||
$this->name = $name;
|
|
||||||
$this->unique = $unique;
|
|
||||||
$this->description = $description;
|
|
||||||
$this->owner = $owner;
|
|
||||||
$this->editors = $editors;
|
|
||||||
$this->members = $members;
|
|
||||||
$this->games = $games;
|
|
||||||
$this->groupMgr = &$groupMgr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Group from array
|
|
||||||
static function fromArray(GroupMgr &$groupMgr, array $a): Group
|
|
||||||
{
|
|
||||||
$id = $a["_id"] ?? -1;
|
|
||||||
return new Group($groupMgr, $a["groupname"], $a["description"], $a["owner"], $id, $a["unique"], $a["editors"], $a["users"], $a["games"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert Group to array
|
|
||||||
function toArray(array $omit = []): array
|
|
||||||
{
|
|
||||||
$a = [
|
|
||||||
"_id" => $this->_id,
|
|
||||||
"groupname" => $this->name,
|
|
||||||
"unique" => $this->unique,
|
|
||||||
"description" => $this->description,
|
|
||||||
"owner" => $this->owner,
|
|
||||||
"editors" => $this->editors,
|
|
||||||
"users" => $this->members,
|
|
||||||
"games" => $this->games
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($omit as $field) {
|
|
||||||
unset($a[$field]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get group's ID.
|
|
||||||
function getID(): int
|
|
||||||
{
|
|
||||||
return $this->_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get group's name.
|
|
||||||
function getName(): string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set group's name.
|
|
||||||
function setName(string $name): void
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
$this->storeMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell if group is unique
|
|
||||||
function isUnique(): bool
|
|
||||||
{
|
|
||||||
return $this->unique;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get group's description.
|
|
||||||
function getDescription(): string
|
|
||||||
{
|
|
||||||
return $this->description;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set group's description.
|
|
||||||
function setDescription(string $description): void
|
|
||||||
{
|
|
||||||
$this->description = $description;
|
|
||||||
$this->storeMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get group's owner.
|
|
||||||
function getOwner(): string
|
|
||||||
{
|
|
||||||
return $this->owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set group's owner.
|
|
||||||
function setOwner(string $owner): void
|
|
||||||
{
|
|
||||||
$this->owner = $owner;
|
|
||||||
$this->storeMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get list of editors.
|
|
||||||
function getEditors(): array
|
|
||||||
{
|
|
||||||
return $this->editors;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set editors.
|
|
||||||
function setEditors(array $editors): void
|
|
||||||
{
|
|
||||||
$this->editors = $editors;
|
|
||||||
$this->storeMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get group members.
|
|
||||||
function getMembers(): array
|
|
||||||
{
|
|
||||||
return $this->members;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set group members.
|
|
||||||
function setMembers(array $members): void
|
|
||||||
{
|
|
||||||
$this->members = $members;
|
|
||||||
$this->storeMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get games.
|
|
||||||
function getGames(): array
|
|
||||||
{
|
|
||||||
return $this->games;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set games.
|
|
||||||
function setGames(array $games): void
|
|
||||||
{
|
|
||||||
$this->games = $games;
|
|
||||||
$this->storeMods();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include/exclude members.
|
|
||||||
function changeMembers(array $nicknames_add, array $nicknames_remove): void
|
|
||||||
{
|
|
||||||
foreach ($nicknames_add as $nickname) { // add members
|
|
||||||
alter_array_contents($this->members, $nickname, null);
|
|
||||||
}
|
|
||||||
foreach ($nicknames_remove as $nickname) { // remove members
|
|
||||||
alter_array_contents($this->members, null, $nickname); // delete from members
|
|
||||||
alter_array_contents($this->editors, null, $nickname); // delete from editors
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storeMods(); // store changes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add members
|
|
||||||
function addMembers(array $nicknames) : void {
|
|
||||||
$this->changeMembers($nicknames, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove members
|
|
||||||
function removeMembers(array $nicknames) : void {
|
|
||||||
$this->changeMembers([], $nicknames);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include/exclude games.
|
|
||||||
function changeGames(array $gameids_add, array $gameids_remove): void
|
|
||||||
{
|
|
||||||
foreach ($gameids_add as $gameid) { // add games
|
|
||||||
alter_array_contents($this->games, $gameid, null);
|
|
||||||
}
|
|
||||||
foreach ($gameids_remove as $gameid) { // remove games
|
|
||||||
alter_array_contents($this->games, null, $gameid);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->storeMods(); // store changes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns whether the user is an editor of this group.
|
|
||||||
function isUserEditor(string $nickname): bool
|
|
||||||
{
|
|
||||||
return in_array($nickname, $this->editors);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns whether the user is an editor or the owner of the group.
|
|
||||||
function isUserContributor(string $nickname): bool
|
|
||||||
{
|
|
||||||
return $this->isUserEditor($nickname) || ($this->owner === $nickname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns if user is member of the group.
|
|
||||||
function isMember(string $nickname): bool
|
|
||||||
{
|
|
||||||
return in_array($nickname, $this->members);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return if game is assigned to this group.
|
|
||||||
function isGameAssigned(string $gameid): bool
|
|
||||||
{
|
|
||||||
return in_array($gameid, $this->games);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get groups unique name.
|
|
||||||
function getUniqueName(): string
|
|
||||||
{
|
|
||||||
return $this->name . ($this->unique ? "" : ("#" . $this->_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupMgr
|
class GroupMgr
|
||||||
{
|
{
|
||||||
|
|||||||
153
class/LogicFunction.php
Normal file
153
class/LogicFunction.php
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||||
|
|
||||||
|
class LogicFunction
|
||||||
|
{
|
||||||
|
public array $input_vars;
|
||||||
|
public string $verilog_form;
|
||||||
|
public string $tex_form;
|
||||||
|
|
||||||
|
public function __construct(array $input_vars = [], string $verilog_form = "", string $tex_form = "")
|
||||||
|
{
|
||||||
|
$this->input_vars = $input_vars;
|
||||||
|
$this->verilog_form = $verilog_form;
|
||||||
|
$this->tex_form = $tex_form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTruthTable(): array {
|
||||||
|
$tt = [];
|
||||||
|
|
||||||
|
$N = count($this->input_vars);
|
||||||
|
$M = pow(2, $N);
|
||||||
|
|
||||||
|
$exp_lang = new ExpressionLanguage();
|
||||||
|
|
||||||
|
$vars = [];
|
||||||
|
foreach ($this->input_vars as $var) {
|
||||||
|
$vars[$var] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cooked_form = str_replace(["&", "|", "~"], ["&&", "||", "!"], $this->verilog_form);
|
||||||
|
printf("Cooked: %s\n", $cooked_form);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $M; $i++) {
|
||||||
|
for ($k = 0; $k < $N; $k++) {
|
||||||
|
$vars[$this->input_vars[$k]] = (($i >> ($N - $k - 1)) & 1) === 1;
|
||||||
|
printf("%d ", $vars[$this->input_vars[$k]]);
|
||||||
|
}
|
||||||
|
$out = $exp_lang->evaluate($cooked_form, $vars);
|
||||||
|
printf("%d\n", $out);
|
||||||
|
$tt[] = $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function genRandom(array $input_vars, int $min_depth = 2, int $max_depth = 3): LogicFunction
|
||||||
|
{
|
||||||
|
function genTerm(array $vars, int $ftn, int $tn, int $mind, int $maxd, bool $top = true, int $opindex = 1): array
|
||||||
|
{
|
||||||
|
$verilog_term = "";
|
||||||
|
$tex_term = "";
|
||||||
|
$m = max($ftn, random_int(1, $tn));
|
||||||
|
if ((($maxd === 0) || ($m === 1)) && ($ftn === 0)) {
|
||||||
|
$neg = random_int(0, 1) === 1;
|
||||||
|
$var = $vars[array_rand($vars, 1)];
|
||||||
|
$verilog_term = ($neg ? "~" : "") . $var;
|
||||||
|
$tex_term = $neg ? ("\\overline{" . $var . "}") : $var;
|
||||||
|
} else {
|
||||||
|
$depth = random_int(0, max(0, $maxd - 1));
|
||||||
|
|
||||||
|
$verilog_ops = [" & ", " | "];
|
||||||
|
$tex_ops = ["", " | "];
|
||||||
|
|
||||||
|
$verilog_op = $verilog_ops[$opindex];
|
||||||
|
$tex_op = $tex_ops[$opindex];
|
||||||
|
|
||||||
|
$verilog_term = !$top ? "(" : "";
|
||||||
|
$tex_term = !$top ? "\\left(" : "";
|
||||||
|
|
||||||
|
$nextopindex = ($opindex === 0) ? 1 : 0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $m; $i++) {
|
||||||
|
$term = genTerm($vars, (($mind - 1) > 0) ? $ftn : 0, $tn, $mind - 1, $depth, false, $nextopindex);
|
||||||
|
$verilog_term .= $term["verilog"];
|
||||||
|
$tex_term .= $term["tex"];
|
||||||
|
if ($i < $m - 1) {
|
||||||
|
$verilog_term .= $verilog_op;
|
||||||
|
$tex_term .= $tex_op;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$verilog_term .= !$top ? ")" : "";
|
||||||
|
$tex_term .= !$top ? "\\right)" : "";
|
||||||
|
}
|
||||||
|
return ["verilog" => $verilog_term, "tex" => $tex_term];
|
||||||
|
}
|
||||||
|
|
||||||
|
$term = genTerm($input_vars, count($input_vars), count($input_vars), $min_depth, $max_depth);
|
||||||
|
|
||||||
|
return new LogicFunction($input_vars, $term["verilog"], $term["tex"]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function genRandomDF($input_vars): LogicFunction
|
||||||
|
{
|
||||||
|
$N = count($input_vars);
|
||||||
|
$states = pow(2, $N);
|
||||||
|
|
||||||
|
$verilog_term = "";
|
||||||
|
$tex_term = "";
|
||||||
|
for ($i = 0; $i < $states; $i++) {
|
||||||
|
|
||||||
|
$verilog_inside = "";
|
||||||
|
$tex_inside = "";
|
||||||
|
|
||||||
|
$omit = random_int(0, 1); // omit the variable or not?
|
||||||
|
|
||||||
|
if (!$omit) {
|
||||||
|
for ($j = 0; $j < $N; $j++) {
|
||||||
|
$neg = !($i & (1 << $j)); // is it an inverted variable?
|
||||||
|
$term = $input_vars[$j];
|
||||||
|
if ($neg) {
|
||||||
|
$verilog_inside .= "~" . $term;
|
||||||
|
$tex_inside .= "\\overline{" . $term . "}";
|
||||||
|
} else {
|
||||||
|
$verilog_inside .= $term;
|
||||||
|
$tex_inside .= $term;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($j < ($N - 1)) {
|
||||||
|
$verilog_inside .= " & ";
|
||||||
|
$tex_inside .= "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//$verilog_inside = rtrim($verilog_inside, "&");
|
||||||
|
//$tex_inside = rtrim($tex_inside, "\\&");
|
||||||
|
|
||||||
|
if ($verilog_inside !== "") {
|
||||||
|
$verilog_term .= "(";
|
||||||
|
$tex_term .= "\\left(";
|
||||||
|
|
||||||
|
$verilog_term .= $verilog_inside;
|
||||||
|
$tex_term .= $tex_inside;
|
||||||
|
|
||||||
|
$verilog_term .= ")";
|
||||||
|
$tex_term .= "\\right)";
|
||||||
|
|
||||||
|
if (($i < ($states - 1)) && !$omit) {
|
||||||
|
$verilog_term .= " | ";
|
||||||
|
$tex_term .= " | ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$verilog_term = rtrim($verilog_term, "| ");
|
||||||
|
$tex_term = rtrim($tex_term, "| ");
|
||||||
|
|
||||||
|
|
||||||
|
return new LogicFunction($input_vars, $verilog_term, $tex_term);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -79,7 +79,7 @@ class Answer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChallengeReport
|
class TaskReport
|
||||||
{
|
{
|
||||||
private string $question;
|
private string $question;
|
||||||
private array $answers;
|
private array $answers;
|
||||||
@ -143,21 +143,21 @@ class ChallengeReport
|
|||||||
class ReportSection
|
class ReportSection
|
||||||
{
|
{
|
||||||
private string $title;
|
private string $title;
|
||||||
private array $challenges;
|
private array $tasks;
|
||||||
|
|
||||||
private function getNumberOfSubmissions() : int {
|
private function getNumberOfSubmissions() : int {
|
||||||
return count($this->challenges) > 0 ? $this->challenges[0]->getSubmissionCount() : 0;
|
return count($this->tasks) > 0 ? $this->tasks[0]->getSubmissionCount() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function __construct(string $title, array $challenges)
|
function __construct(string $title, array $tasks)
|
||||||
{
|
{
|
||||||
$this->title = $title;
|
$this->title = $title;
|
||||||
$this->challenges = array_map(fn($ch) => new ChallengeReport($ch), $challenges);
|
$this->tasks = array_map(fn($ch) => new TaskReport($ch), $tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChallenges(): array
|
function getTasks(): array
|
||||||
{
|
{
|
||||||
return $this->challenges;
|
return $this->tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTitle(): string
|
function getTitle(): string
|
||||||
@ -169,8 +169,8 @@ class ReportSection
|
|||||||
function genTeX(): string
|
function genTeX(): string
|
||||||
{
|
{
|
||||||
$tex = "\\begin{quiz}{" . $this->title . "}{" . $this->getNumberOfSubmissions() . "}\n";
|
$tex = "\\begin{quiz}{" . $this->title . "}{" . $this->getNumberOfSubmissions() . "}\n";
|
||||||
foreach ($this->challenges as $challenge) {
|
foreach ($this->tasks as $task) {
|
||||||
$tex .= $challenge->genTeX();
|
$tex .= $task->genTeX();
|
||||||
}
|
}
|
||||||
$tex .= "\\end{quiz}\n";
|
$tex .= "\\end{quiz}\n";
|
||||||
return $tex;
|
return $tex;
|
||||||
|
|||||||
144
class/Task.php
Normal file
144
class/Task.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Task implements JsonSerializable
|
||||||
|
{
|
||||||
|
protected string $type; // task type
|
||||||
|
protected string $question; // the task title
|
||||||
|
protected mixed $player_answer; // answer given by the player
|
||||||
|
protected mixed $correct_answer;
|
||||||
|
protected float $max_mark; // maximum points that can be collected at this task
|
||||||
|
protected bool $is_template; // this task is a template
|
||||||
|
protected array $flags; // task flags
|
||||||
|
|
||||||
|
function __construct(string $type, array &$a = null)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
$this->is_template = $a["is_template"] ?? false;
|
||||||
|
$this->max_mark = $a["max_mark"] ?? 1.0;
|
||||||
|
$this->question = $a["question"] ?? "";
|
||||||
|
$this->flags = $a["flags"] ?? [];
|
||||||
|
$this->player_answer = $a["player_answer"] ?? null;
|
||||||
|
$this->correct_answer = $a["correct_answer"] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQuestion(string $question): void
|
||||||
|
{
|
||||||
|
$this->question = $question;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuestion(): string
|
||||||
|
{
|
||||||
|
return $this->question;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save answer
|
||||||
|
function saveAnswer(mixed $ans): bool
|
||||||
|
{
|
||||||
|
$this->player_answer = $ans;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear answer
|
||||||
|
function clearAnswer(): void
|
||||||
|
{
|
||||||
|
$this->player_answer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// set task type
|
||||||
|
function setType(string $type): void {
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get task type
|
||||||
|
function getType(): string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMaxMark(float $max_mark): void
|
||||||
|
{
|
||||||
|
$this->max_mark = $max_mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxMark(): float
|
||||||
|
{
|
||||||
|
return $this->max_mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMark(): float
|
||||||
|
{
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray(): array
|
||||||
|
{
|
||||||
|
$a = [
|
||||||
|
"type" => $this->type,
|
||||||
|
"question" => $this->question,
|
||||||
|
"max_mark" => $this->max_mark,
|
||||||
|
"is_template" => $this->is_template,
|
||||||
|
"flags" => $this->flags,
|
||||||
|
"correct_answer" => $this->correct_answer,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$this->isTemplate()) {
|
||||||
|
$a["player_answer"] = $this->player_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonSerialize(): mixed
|
||||||
|
{
|
||||||
|
return $this->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTemplate(bool $is_template): void
|
||||||
|
{
|
||||||
|
$this->is_template = $is_template;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTemplate(): bool
|
||||||
|
{
|
||||||
|
return $this->is_template;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFlags(): array
|
||||||
|
{
|
||||||
|
return $this->flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlags(array $flags): void
|
||||||
|
{
|
||||||
|
$this->flags = $flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFlag(string $flag): bool
|
||||||
|
{
|
||||||
|
return in_array($flag, $this->flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayerAnswer(): mixed {
|
||||||
|
return $this->player_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /* FIXME: ez ugyanaz, mint a saveAnswer() */
|
||||||
|
// function setPlayerAnswer(mixed $player_answer): void {
|
||||||
|
// $this->player_answer = $player_answer;
|
||||||
|
// }
|
||||||
|
|
||||||
|
function setCorrectAnswer(mixed $correct_answer): void
|
||||||
|
{
|
||||||
|
$this->correct_answer = $correct_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCorrectAnswer(): mixed
|
||||||
|
{
|
||||||
|
return $this->correct_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomize(): void
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
class/TaskFactory.php
Normal file
36
class/TaskFactory.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "Tasks/SingleChoiceTask.php";
|
||||||
|
|
||||||
|
require_once "Tasks/OpenEndedTask.php";
|
||||||
|
|
||||||
|
require_once "Tasks/NumberConversionTask.php";
|
||||||
|
|
||||||
|
class TaskFactory
|
||||||
|
{
|
||||||
|
static function fromArray(array $a): Task|null
|
||||||
|
{
|
||||||
|
$type = $a["type"] ?? "singlechoice"; // if the type is missing, then it's a single choice task
|
||||||
|
switch ($type) {
|
||||||
|
case "singlechoice":
|
||||||
|
return new SingleChoiceTask($a);
|
||||||
|
break;
|
||||||
|
case "openended":
|
||||||
|
return new OpenEndedTask($a);
|
||||||
|
break;
|
||||||
|
case "numberconversion":
|
||||||
|
return new NumberConversionTask($a);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function constructFromCollection(array $c): array {
|
||||||
|
$chgs = [];
|
||||||
|
foreach ($c as $ch) {
|
||||||
|
$chgs[] = TaskFactory::fromArray($ch);
|
||||||
|
}
|
||||||
|
return $chgs;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
class/Tasks/NumberConversionTask.php
Normal file
159
class/Tasks/NumberConversionTask.php
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "OpenEndedTask.php";
|
||||||
|
|
||||||
|
class NumberConversionTask extends OpenEndedTask
|
||||||
|
{
|
||||||
|
protected string $instruction; // instruction word
|
||||||
|
protected string $source; // source
|
||||||
|
|
||||||
|
// runtime variables -----
|
||||||
|
private int $src_base; // source number system
|
||||||
|
private string $src_rep; // source representation
|
||||||
|
private int $src_n_digits; // minimum number of digits in the source
|
||||||
|
private int $dst_base; // destination number system
|
||||||
|
private string $dst_rep; // destination representation
|
||||||
|
private int $dst_n_digits; // number of digits in the destination
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
public function __construct(array &$a = null)
|
||||||
|
{
|
||||||
|
parent::__construct($a);
|
||||||
|
|
||||||
|
$this->setType("numberconversion");
|
||||||
|
|
||||||
|
// get instruction word
|
||||||
|
$this->instruction = strtolower(trim($a["instruction"] ?? "10u:2->2u:4"));
|
||||||
|
|
||||||
|
// expand it
|
||||||
|
$pattern = "/([0-9]+)([suc]):([0-9]+)->([0-9]+)([suc]):([0-9]+)/";
|
||||||
|
preg_match($pattern, $this->instruction, $matches);
|
||||||
|
|
||||||
|
// if the instruction was meaningful
|
||||||
|
if (count($matches) == 7) {
|
||||||
|
$this->src_base = (int)$matches[1];
|
||||||
|
$this->src_rep = $matches[2];
|
||||||
|
$this->src_n_digits = $matches[3];
|
||||||
|
$this->dst_base = (int)$matches[4];
|
||||||
|
$this->dst_rep = $matches[5];
|
||||||
|
$this->dst_n_digits = $matches[6];
|
||||||
|
} else { // no valid instruction word has been passed
|
||||||
|
$this->src_base = 10;
|
||||||
|
$this->src_rep = "u";
|
||||||
|
$this->src_n_digits = 2;
|
||||||
|
$this->dst_base = 2;
|
||||||
|
$this->dst_rep = "u";
|
||||||
|
$this->dst_n_digits = 4;
|
||||||
|
|
||||||
|
$this->instruction = $this->src_base . $this->src_rep . ":" . $this->src_n_digits . "->" . $this->dst_base . $this->dst_rep . ":" . $this->dst_n_digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->source = $a["source"] ?? "---";
|
||||||
|
$this->correct_answer = $a["correct_answer"] ?? "---";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$a = parent::toArray();
|
||||||
|
|
||||||
|
$a["instruction"] = $this->instruction;
|
||||||
|
$a["source"] = $this->source;
|
||||||
|
$a["correct_answer"] = $this->correct_answer;
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function extend(string $num, int $base, int $exnd, bool $comp): string
|
||||||
|
{
|
||||||
|
$fd = (int)(base_convert($num[0], $base, 10)); // get first digit as a number
|
||||||
|
$extd = (string)(($comp && ($fd >= ($base / 2))) ? ($base - 1) : 0); // get the extension digit
|
||||||
|
return str_pad((string)($num), $extd, $extd, STR_PAD_LEFT); // extend to the left
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function complement(string $num, int $base): string
|
||||||
|
{
|
||||||
|
// convert to an integer
|
||||||
|
$M = (int)(base_convert($num, $base, 10));
|
||||||
|
|
||||||
|
// check if the highest digit is less than the half of the base, if not, add a zero prefix
|
||||||
|
$fd = (int)(base_convert($num[0], $base, 10));
|
||||||
|
|
||||||
|
// create the basis for forming the complement
|
||||||
|
$H = (string)($base - 1);
|
||||||
|
$K_str = (int)(str_repeat($H, strlen($num)));
|
||||||
|
if ($fd >= ($base / 2)) { // if one more digit is needed...
|
||||||
|
$K_str = $H . $K_str; // prepend with a zero digit
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to integer
|
||||||
|
$K = (int)(base_convert($K_str, $base, 10));
|
||||||
|
|
||||||
|
// form the base's complement
|
||||||
|
$C = $K - $M + 1;
|
||||||
|
|
||||||
|
// convert to the final base
|
||||||
|
return base_convert((string)$C, 10, $base);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function changeRepresentation(int $num, int $base, string $rep, int $digits): string {
|
||||||
|
$neg = $num < 0; // store if the value is negative
|
||||||
|
$numa_str = (string)(abs($num)); // create the absolute value as a string
|
||||||
|
$numa_str = base_convert($numa_str, 10, $base); // convert to specific base
|
||||||
|
if ($neg) {
|
||||||
|
if ($rep === "s") {
|
||||||
|
$numa_str = self::extend($numa_str, $base, $digits, false);
|
||||||
|
$numa_str = "-" . $numa_str;
|
||||||
|
} else if ($rep === "c") {
|
||||||
|
$numa_str = self::complement($numa_str, $rep);
|
||||||
|
$numa_str = self::extend($numa_str, $base, $digits, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$numa_str = self::extend($numa_str, $base, $digits, false);
|
||||||
|
}
|
||||||
|
return $numa_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function randomize(): void
|
||||||
|
{
|
||||||
|
// validate representation marks
|
||||||
|
$invalid = in_array($this->src_rep . $this->dst_rep, ["us", "su"]);
|
||||||
|
if ($invalid) { // fix invalid representation pairs
|
||||||
|
$this->dst_rep = "u";
|
||||||
|
$this->src_rep = "u";
|
||||||
|
}
|
||||||
|
|
||||||
|
// specify the range
|
||||||
|
$max = 1;
|
||||||
|
$min = 0;
|
||||||
|
switch ($this->dst_rep) {
|
||||||
|
case "u":
|
||||||
|
$max = pow($this->dst_base, $this->dst_n_digits) - 1;
|
||||||
|
$min = 0;
|
||||||
|
break;
|
||||||
|
case "s":
|
||||||
|
$max = pow($this->dst_base, $this->dst_n_digits) - 1;
|
||||||
|
$min = -$max;
|
||||||
|
break;
|
||||||
|
case "c":
|
||||||
|
$max = pow($this->dst_base, $this->dst_n_digits - 1) - 1;
|
||||||
|
$min = -($max + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomize a value
|
||||||
|
$m = random_int($min, $max);
|
||||||
|
|
||||||
|
// create the question and the answer
|
||||||
|
$this->correct_answer = self::changeRepresentation($m, $this->dst_base, $this->dst_rep, $this->dst_n_digits);
|
||||||
|
$this->source = self::changeRepresentation($m, $this->src_base, $this->src_rep, $this->src_n_digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMark(): float
|
||||||
|
{
|
||||||
|
if ($this->hasFlag("acceptwithoutleadingzeros")) {
|
||||||
|
return (ltrim($this->player_answer, " 0") === ltrim($this->correct_answer, "0")) ? 1.0 : 0.0;
|
||||||
|
} else {
|
||||||
|
return (trim($this->player_answer) === trim($this->correct_answer)) ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
class/Tasks/OpenEndedTask.php
Normal file
63
class/Tasks/OpenEndedTask.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "PicturedTask.php";
|
||||||
|
|
||||||
|
class OpenEndedTask extends PicturedTask
|
||||||
|
{
|
||||||
|
public function __construct(array $a = null)
|
||||||
|
{
|
||||||
|
parent::__construct("openended", $a);
|
||||||
|
|
||||||
|
$this->correct_answer = $a["correct_answer"] ?? null;
|
||||||
|
$this->setMaxMark(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addCorrectAnswer(string $ca): void {
|
||||||
|
$this->correct_answer[] = $ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveAnswer(mixed $ans): bool
|
||||||
|
{
|
||||||
|
// collect transformations
|
||||||
|
$transform_fns = [];
|
||||||
|
foreach ($this->flags as $flag) {
|
||||||
|
switch ($flag) {
|
||||||
|
case "makeuppercase":
|
||||||
|
$transform_fns[] = "strtoupper";
|
||||||
|
break;
|
||||||
|
case "makelowercase":
|
||||||
|
$transform_fns[] = "strtolower";
|
||||||
|
break;
|
||||||
|
case "removespaces":
|
||||||
|
$transform_fns[] = fn($str) => str_replace(" ", "", $str);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the transformations
|
||||||
|
foreach ($transform_fns as $tfn) {
|
||||||
|
$ans = $tfn($ans);
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the answer
|
||||||
|
$this->player_answer = $ans;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearAnswer(): void
|
||||||
|
{
|
||||||
|
$this->player_answer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMark(): float
|
||||||
|
{
|
||||||
|
return in_array($this->player_answer, $this->correct_answer) ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray(): array {
|
||||||
|
$a = parent::toArray();
|
||||||
|
$a["correct_answer"] = $this->correct_answer;
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
class/Tasks/PicturedTask.php
Normal file
31
class/Tasks/PicturedTask.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "class/Task.php";
|
||||||
|
|
||||||
|
class PicturedTask extends Task
|
||||||
|
{
|
||||||
|
protected string $image_url; // the URL of the corresponding image
|
||||||
|
|
||||||
|
function __construct(string $type, array &$a = null)
|
||||||
|
{
|
||||||
|
parent::__construct($type, $a);
|
||||||
|
$this->image_url = $a["image_url"] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImageUrl(string $image_url): void
|
||||||
|
{
|
||||||
|
$this->image_url = $image_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageUrl(): string
|
||||||
|
{
|
||||||
|
return $this->image_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray(): array
|
||||||
|
{
|
||||||
|
$a = parent::toArray();
|
||||||
|
$a["image_url"] = $this->image_url;
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
}
|
||||||
85
class/Tasks/SingleChoiceTask.php
Normal file
85
class/Tasks/SingleChoiceTask.php
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "PicturedTask.php";
|
||||||
|
|
||||||
|
class SingleChoiceTask extends PicturedTask
|
||||||
|
{
|
||||||
|
private array $answers; // possible answers
|
||||||
|
|
||||||
|
// -----------------
|
||||||
|
|
||||||
|
function __construct(array $a = null)
|
||||||
|
{
|
||||||
|
parent::__construct("singlechoice", $a);
|
||||||
|
|
||||||
|
$this->answers = $a["answers"] ?? [];
|
||||||
|
$ca = $a["correct_answer"] ?? -1;
|
||||||
|
if (gettype($ca) === "string") { // backward compatibility
|
||||||
|
$this->correct_answer = array_search($a["correct_answer"], $this->answers);
|
||||||
|
} else {
|
||||||
|
$this->correct_answer = (int)($ca);
|
||||||
|
}
|
||||||
|
$this->player_answer = $a["player_answer"] ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAnswer(string $answer): void
|
||||||
|
{
|
||||||
|
$this->answers[] = $answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnswers(): array
|
||||||
|
{
|
||||||
|
return $this->answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAnswerIdInsideBounds($ansid): bool
|
||||||
|
{
|
||||||
|
return ($ansid >= 0) && ($ansid <= count($this->answers));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAnswer(mixed $ans): bool
|
||||||
|
{
|
||||||
|
$ansidx = (int)($ans); // cast answer to integer as it is a number
|
||||||
|
if ($this->isAnswerIdInsideBounds($ansidx)) {
|
||||||
|
$this->player_answer = $ansidx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAnswer(): void
|
||||||
|
{
|
||||||
|
$this->player_answer = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMark(): float
|
||||||
|
{
|
||||||
|
return ($this->player_answer == $this->correct_answer) ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray(): array
|
||||||
|
{
|
||||||
|
$a = parent::toArray();
|
||||||
|
$a["answers"] = $this->answers;
|
||||||
|
$a["correct_answer"] = $this->correct_answer;
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomize(): void{
|
||||||
|
$ordering = range(0, count($this->answers) - 1); // create an ordered range
|
||||||
|
shuffle($ordering); // shuffle indices
|
||||||
|
$shans = [];
|
||||||
|
$ca_remapped = false; // avoid remapping the correct answer multiple times
|
||||||
|
for ($i = 0; $i < count($this->answers); $i++) { // shuffle answers
|
||||||
|
$orig_pos = $ordering[$i];
|
||||||
|
$shans[] = $this->answers[$orig_pos];
|
||||||
|
if ((!$ca_remapped) && ($orig_pos == $this->correct_answer)) {
|
||||||
|
$this->correct_answer = $i;
|
||||||
|
$ca_remapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
$this->answers = $shans;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
class/Tasks/TruthTableTask.php
Normal file
18
class/Tasks/TruthTableTask.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "OpenEndedTask.php";
|
||||||
|
|
||||||
|
class TruthTableTask extends OpenEndedTask
|
||||||
|
{
|
||||||
|
public function __construct(array $a = null)
|
||||||
|
{
|
||||||
|
parent::__construct($a);
|
||||||
|
|
||||||
|
$this->setType("verilog");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function randomize(): void
|
||||||
|
{
|
||||||
|
return parent::randomize(); // TODO: Change the autogenerated stub
|
||||||
|
}
|
||||||
|
}
|
||||||
251
class/Test.php
Normal file
251
class/Test.php
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "TaskFactory.php";
|
||||||
|
|
||||||
|
require_once "TestSummary.php";
|
||||||
|
|
||||||
|
class Test extends AutoStoring
|
||||||
|
{
|
||||||
|
const TEST_ONGOING = "ongoing";
|
||||||
|
const TEST_CONCLUDED = "concluded";
|
||||||
|
|
||||||
|
// ---------
|
||||||
|
|
||||||
|
public int $id; // ID
|
||||||
|
public int $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 $tasks; // Test tasks
|
||||||
|
|
||||||
|
private TestMgr $testMgr; // Reference to TestMgr managing this Test instance
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
// Preprocess tasks.
|
||||||
|
private function preprocessTasks(): void
|
||||||
|
{
|
||||||
|
foreach ($this->tasks as &$task) {
|
||||||
|
$task->setTemplate(false); // the task is no longer a template
|
||||||
|
$task->randomize(); // randomize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
function getMaxSumMark(): float
|
||||||
|
{
|
||||||
|
$msm = 0.0;
|
||||||
|
foreach ($this->tasks as &$task) {
|
||||||
|
$msm += $task->getMaxMark();
|
||||||
|
}
|
||||||
|
return $msm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
// 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"] ?? 0;
|
||||||
|
$this->repeatable = $a["repeatable"];
|
||||||
|
$this->tasks = TaskFactory::constructFromCollection($a["challenges"]);
|
||||||
|
if (isset($a["summary"])) {
|
||||||
|
$this->summary = TestSummary::fromArray($a["summary"]);
|
||||||
|
} else { // backward compatibility
|
||||||
|
$this->summary = new TestSummary($this->getMaxSumMark(), 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->tasks = $game->getTasks();
|
||||||
|
$this->preprocessTasks();
|
||||||
|
$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"];
|
||||||
|
} else {
|
||||||
|
$this->endLimitTime = -1; // dummy value, not used, since timeLimited is false
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repeatable = $gp["repeatable"];
|
||||||
|
|
||||||
|
// Create a blank summary
|
||||||
|
$this->summary = new TestSummary($this->getMaxSumMark(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-conclude time-constrained test if expired
|
||||||
|
if ($this->timeLimited && $this->isOngoing() && ($this->endLimitTime <= time())) {
|
||||||
|
$this->concludeTest();
|
||||||
|
$this->endTime = $this->endLimitTime; // date back end time to the limiting value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert test to array.
|
||||||
|
function toArray(array $omit = []): array
|
||||||
|
{
|
||||||
|
$tasks = [];
|
||||||
|
foreach ($this->tasks as &$t) {
|
||||||
|
$tasks[] = $t->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
$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" => $tasks,
|
||||||
|
"summary" => $this->summary->toArray()
|
||||||
|
];
|
||||||
|
|
||||||
|
// omit specific fields
|
||||||
|
foreach ($omit as $field) {
|
||||||
|
unset($a[$field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get number of tasks.
|
||||||
|
function getTaskCount(): int
|
||||||
|
{
|
||||||
|
return count($this->tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTaskIdInsideBounds(int $tidx): bool {
|
||||||
|
return ($tidx >= 0) && ($tidx < $this->getTaskCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save answer. Asserting $safe prevents saving answers to a concluded test.
|
||||||
|
function saveAnswer(int $tidx, int|string $ans, bool $safe = true): bool
|
||||||
|
{
|
||||||
|
if (!$safe || $this->state === self::TEST_ONGOING) {
|
||||||
|
if ($this->isTaskIdInsideBounds($tidx)) {
|
||||||
|
$this->tasks[$tidx]->saveAnswer($ans);
|
||||||
|
$this->commitMods();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear answer.
|
||||||
|
function clearAnswer(int $chidx, bool $safe = true): bool
|
||||||
|
{
|
||||||
|
if (!$safe || $this->state === self::TEST_ONGOING) {
|
||||||
|
if ($this->isTaskIdInsideBounds($chidx)) {
|
||||||
|
$this->tasks[$chidx]->clearAnswer();
|
||||||
|
$this->commitMods();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conclude test.
|
||||||
|
function concludeTest(): void
|
||||||
|
{
|
||||||
|
// summarize points
|
||||||
|
$mark_sum = 0.0;
|
||||||
|
foreach ($this->tasks as &$ch) {
|
||||||
|
$mark_sum += $ch->getMark();
|
||||||
|
}
|
||||||
|
|
||||||
|
// set state and fill summary
|
||||||
|
$this->state = self::TEST_CONCLUDED;
|
||||||
|
$this->endTime = time();
|
||||||
|
$this->summary->setMark($mark_sum);
|
||||||
|
|
||||||
|
// 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(): int
|
||||||
|
{
|
||||||
|
return $this->gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConcluded(): bool
|
||||||
|
{
|
||||||
|
return $this->state === self::TEST_CONCLUDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOngoing(): bool
|
||||||
|
{
|
||||||
|
return $this->state === self::TEST_ONGOING;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,534 +8,7 @@ require_once "ExpressionBuilder.php";
|
|||||||
|
|
||||||
require_once "globals.php";
|
require_once "globals.php";
|
||||||
|
|
||||||
const TEST_ONGOING = "ongoing";
|
require_once "Test.php";
|
||||||
const TEST_CONCLUDED = "concluded";
|
|
||||||
|
|
||||||
class TestSummary
|
|
||||||
{
|
|
||||||
public int $maxMark; // Number of challenges
|
|
||||||
public int $mark; // Number of correct answers
|
|
||||||
private float $percentage; // Ratio of correct answers
|
|
||||||
|
|
||||||
// Calculate percentage.
|
|
||||||
private function calculatePercentage(): void
|
|
||||||
{
|
|
||||||
if ($this->maxMark > 0) {
|
|
||||||
$this->percentage = $this->mark / (double)$this->maxMark * 100.0;
|
|
||||||
} else { // avoid division by zero
|
|
||||||
$this->percentage = 0.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function __construct(int $challengeN, int $correctAnswerN)
|
|
||||||
{
|
|
||||||
$this->maxMark = $challengeN;
|
|
||||||
$this->mark = $correctAnswerN;
|
|
||||||
$this->calculatePercentage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get challenge count.
|
|
||||||
function getMaxMark(): int
|
|
||||||
{
|
|
||||||
return $this->maxMark;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get number of correct answers.
|
|
||||||
function getMark(): int
|
|
||||||
{
|
|
||||||
return $this->mark;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMark(int $mark): void
|
|
||||||
{
|
|
||||||
$this->mark = $mark;
|
|
||||||
$this->calculatePercentage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get ratio of correct results.
|
|
||||||
function getPercentage(): float
|
|
||||||
{
|
|
||||||
return ($this->mark * 100.0) / $this->maxMark;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build from array.
|
|
||||||
static function fromArray(array $a): TestSummary
|
|
||||||
{
|
|
||||||
if (!isset($a["max_mark"]) || !isset($a["mark"])) { // backward compatibility
|
|
||||||
return new TestSummary($a["challenge_n"], $a["correct_answer_n"]);
|
|
||||||
} else {
|
|
||||||
return new TestSummary($a["max_mark"], $a["mark"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to array.
|
|
||||||
function toArray(): array
|
|
||||||
{
|
|
||||||
return ["challenge_n" => $this->maxMark, "correct_answer_n" => $this->mark, "percentage" => $this->percentage];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Challenge
|
|
||||||
{
|
|
||||||
protected string $type; // challenge type
|
|
||||||
|
|
||||||
protected float $max_mark; // maximum points that can be collected at this challenge
|
|
||||||
|
|
||||||
protected bool $is_template; // this challenge is a template
|
|
||||||
|
|
||||||
function __construct(string $type)
|
|
||||||
{
|
|
||||||
$this->type = $type;
|
|
||||||
$this->is_template = false;
|
|
||||||
$this->max_mark = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save answer
|
|
||||||
function saveAnswer(int|string $ans): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear answer
|
|
||||||
function clearAnswer(int|string $ans): bool
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get challenge type
|
|
||||||
function getType(): string
|
|
||||||
{
|
|
||||||
return $this->type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMaxMark(float $max_mark): void
|
|
||||||
{
|
|
||||||
$this->max_mark = $max_mark;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMaxMark(): float {
|
|
||||||
return $this->max_mark;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMark(): float
|
|
||||||
{
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toArray(): array
|
|
||||||
{
|
|
||||||
return ["type" => $this->type];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTemplate(bool $is_template): void
|
|
||||||
{
|
|
||||||
$this->is_template = $is_template;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTemplate(): bool
|
|
||||||
{
|
|
||||||
return $this->is_template;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomize(): void {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PicturedChallenge extends Challenge
|
|
||||||
{
|
|
||||||
protected string $image_url; // the URL of the corresponding image
|
|
||||||
|
|
||||||
function __construct(string $type, array $a = null)
|
|
||||||
{
|
|
||||||
parent::__construct($type);
|
|
||||||
$this->image_url = $a["image_url"] ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function setImageUrl(string $image_url): void
|
|
||||||
{
|
|
||||||
$this->image_url = $image_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageUrl(): string
|
|
||||||
{
|
|
||||||
return $this->image_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toArray(): array
|
|
||||||
{
|
|
||||||
$a = parent::toArray();
|
|
||||||
$a["image_url"] = $this->image_url;
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SingleChoiceChallenge extends PicturedChallenge
|
|
||||||
{
|
|
||||||
private string $question; // the task title
|
|
||||||
private array $answers; // possible answers
|
|
||||||
private int $correct_answer; // the single correct answer
|
|
||||||
|
|
||||||
private int $player_answer; // answer given by the player
|
|
||||||
|
|
||||||
// -----------------
|
|
||||||
|
|
||||||
function __construct(array $a = null)
|
|
||||||
{
|
|
||||||
parent::__construct("singlechoice", $a);
|
|
||||||
|
|
||||||
$this->question = $a["question"] ?? "";
|
|
||||||
$this->answers = $a["answers"] ?? [];
|
|
||||||
$this->correct_answer = (int)($a["correct_answer"] ?? -1);
|
|
||||||
$this->player_answer = (int)($a["player_answer"] ?? -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setQuestion(string $question): void
|
|
||||||
{
|
|
||||||
$this->question = $question;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQuestion(): string
|
|
||||||
{
|
|
||||||
return $this->question;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAnswer(string $answer): void
|
|
||||||
{
|
|
||||||
$this->answers[] = $answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAnswers(): array
|
|
||||||
{
|
|
||||||
return $this->answers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCorrectAnswer(string $correct_answer): void
|
|
||||||
{
|
|
||||||
$this->correct_answer = $correct_answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCorrectAnswer(): string
|
|
||||||
{
|
|
||||||
return $this->correct_answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isAnswerIdInsideBounds($ansid): bool
|
|
||||||
{
|
|
||||||
return ($ansid >= 0) && ($ansid <= count($this->answers));
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveAnswer(int|string $ans): bool
|
|
||||||
{
|
|
||||||
$ansidx = (int)($ans); // cast answer to integer as it is a number
|
|
||||||
if ($this->isAnswerIdInsideBounds($ansidx)) {
|
|
||||||
$this->player_answer = $ansidx;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAnswer(int|string $ans): bool
|
|
||||||
{
|
|
||||||
$ansidx = (int)($ans); // cast answer to integer as it is a number
|
|
||||||
if ($this->isAnswerIdInsideBounds($ansidx)) {
|
|
||||||
$this->player_answer = -1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMark(): float
|
|
||||||
{
|
|
||||||
return ($this->player_answer == $this->correct_answer) ? 1.0 : 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toArray(): array
|
|
||||||
{
|
|
||||||
$a = parent::toArray();
|
|
||||||
$a["question"] = $this->question;
|
|
||||||
$a["answers"] = $this->answers;
|
|
||||||
$a["correct_answer"] = $this->correct_answer;
|
|
||||||
if (!$this->isTemplate()) {
|
|
||||||
$a["player_answer"] = $this->player_answer;
|
|
||||||
}
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomize(): void{
|
|
||||||
//shuffle($this->answers); // shuffle answers
|
|
||||||
//$this->correct_answer = array_search($this->correct_answer, $this->answers); // remap correct answer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChallengeFactory
|
|
||||||
{
|
|
||||||
static function fromArray(array $a): Challenge|null
|
|
||||||
{
|
|
||||||
$type = $a["type"] ?? "singlechoice"; // if the type is missing, then it's a single choice challenge
|
|
||||||
switch ($type) {
|
|
||||||
case "singlechoice":
|
|
||||||
return new SingleChoiceChallenge($a);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function constructFromCollection(array $c): array {
|
|
||||||
$chgs = [];
|
|
||||||
foreach ($c as $ch) {
|
|
||||||
$chgs[] = ChallengeFactory::fromArray($ch);
|
|
||||||
}
|
|
||||||
return $chgs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Test extends AutoStoring
|
|
||||||
{
|
|
||||||
const TEST_ONGOING = "ongoing";
|
|
||||||
const TEST_CONCLUDED = "concluded";
|
|
||||||
|
|
||||||
// ---------
|
|
||||||
|
|
||||||
public int $id; // ID
|
|
||||||
public int $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) {
|
|
||||||
$ch->randomize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------
|
|
||||||
|
|
||||||
function getMaxSumMark(): float
|
|
||||||
{
|
|
||||||
$msm = 0.0;
|
|
||||||
foreach ($this->challenges as &$ch) {
|
|
||||||
$msm += $ch->getMaxMark();
|
|
||||||
}
|
|
||||||
return $msm;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------
|
|
||||||
|
|
||||||
// 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"] ?? 0;
|
|
||||||
$this->repeatable = $a["repeatable"];
|
|
||||||
$this->challenges = ChallengeFactory::constructFromCollection($a["challenges"]);
|
|
||||||
if (isset($a["summary"])) {
|
|
||||||
$this->summary = TestSummary::fromArray($a["summary"]);
|
|
||||||
} else { // backward compatibility
|
|
||||||
$this->summary = new TestSummary($this->getMaxSumMark(), 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 = ChallengeFactory::constructFromCollection($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"];
|
|
||||||
} else {
|
|
||||||
$this->endLimitTime = -1; // dummy value, not used, since timeLimited is false
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->repeatable = $gp["repeatable"];
|
|
||||||
|
|
||||||
// Create a blank summary
|
|
||||||
$this->summary = new TestSummary($this->getMaxSumMark(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// auto-conclude time-constrained test if expired
|
|
||||||
if ($this->timeLimited && $this->isOngoing() && ($this->endLimitTime <= time())) {
|
|
||||||
$this->concludeTest();
|
|
||||||
$this->endTime = $this->endLimitTime; // date back end time to the limiting value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert test to array.
|
|
||||||
function toArray(array $omit = []): array
|
|
||||||
{
|
|
||||||
$chgs = [];
|
|
||||||
foreach ($this->challenges as $ch) {
|
|
||||||
$chgs[] = $ch->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
$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" => $chgs,
|
|
||||||
"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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isChallengeIdInsideBounds(int $chidx): bool {
|
|
||||||
return ($chidx >= 0) && ($chidx < $this->getChallengeCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save answer. Asserting $safe prevents saving answers to a concluded test.
|
|
||||||
function saveAnswer(int $chidx, string $ans, bool $safe = true): bool
|
|
||||||
{
|
|
||||||
if (!$safe || $this->state === self::TEST_ONGOING) {
|
|
||||||
if ($this->isChallengeIdInsideBounds($chidx)) {
|
|
||||||
$this->challenges[$chidx]->saveAnswer($ans);
|
|
||||||
$this->commitMods();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear answer.
|
|
||||||
function clearAnswer(int $chidx, bool $safe = true): bool
|
|
||||||
{
|
|
||||||
if (!$safe || $this->state === self::TEST_ONGOING) {
|
|
||||||
if ($this->isChallengeIdInsideBounds($chidx)) {
|
|
||||||
$this->challenges[$chidx]->clearAnswer();
|
|
||||||
$this->commitMods();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conclude test.
|
|
||||||
function concludeTest(): void
|
|
||||||
{
|
|
||||||
// summarize points
|
|
||||||
$mark_sum = 0.0;
|
|
||||||
foreach ($this->challenges as &$ch) {
|
|
||||||
$mark_sum += $ch->getMark();
|
|
||||||
}
|
|
||||||
|
|
||||||
// set state and fill summary
|
|
||||||
$this->state = TEST_CONCLUDED;
|
|
||||||
$this->endTime = time();
|
|
||||||
$this->summary->setMark($mark_sum);
|
|
||||||
|
|
||||||
// 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(): int
|
|
||||||
{
|
|
||||||
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
|
class TestMgr
|
||||||
{
|
{
|
||||||
@ -598,7 +71,7 @@ class TestMgr
|
|||||||
if (count($previous_tests) > 0) { // if there are previous attempts, then...
|
if (count($previous_tests) > 0) { // if there are previous attempts, then...
|
||||||
fetch:
|
fetch:
|
||||||
// re-fetch tests, look only for ongoing
|
// re-fetch tests, look only for ongoing
|
||||||
$ongoing_tests = $this->db->findBy([$fetch_criteria, "AND", ["state", "=", TEST_ONGOING]]);
|
$ongoing_tests = $this->db->findBy([$fetch_criteria, "AND", ["state", "=", Test::TEST_ONGOING]]);
|
||||||
if (count($ongoing_tests) !== 0) { // if there's an ongoing test
|
if (count($ongoing_tests) !== 0) { // if there's an ongoing test
|
||||||
$testid = $ongoing_tests[0]["_id"];
|
$testid = $ongoing_tests[0]["_id"];
|
||||||
$test = $this->getTest($testid);
|
$test = $this->getTest($testid);
|
||||||
@ -643,7 +116,7 @@ class TestMgr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get test results by game ID.
|
// Get test results by game ID.
|
||||||
function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_challenge_data, bool $best_ones_only, array ...$furtherFilters): array
|
function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_task_data, bool $best_ones_only, array ...$furtherFilters): array
|
||||||
{
|
{
|
||||||
$qb = $this->db->createQueryBuilder();
|
$qb = $this->db->createQueryBuilder();
|
||||||
$qb = $qb->where(["gameid", "=", (int)$gameid]);
|
$qb = $qb->where(["gameid", "=", (int)$gameid]);
|
||||||
@ -679,8 +152,8 @@ class TestMgr
|
|||||||
$qb->orderBy($ordering);
|
$qb->orderBy($ordering);
|
||||||
}
|
}
|
||||||
|
|
||||||
// excluding challenge data
|
// excluding task data
|
||||||
if ($exclude_challenge_data) {
|
if ($exclude_task_data) {
|
||||||
$qb->except(["challenges"]);
|
$qb->except(["challenges"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -731,42 +204,42 @@ class TestMgr
|
|||||||
$qb->select(["challenges"]);
|
$qb->select(["challenges"]);
|
||||||
$entries = $qb->getQuery()->fetch();
|
$entries = $qb->getQuery()->fetch();
|
||||||
|
|
||||||
$challenge_indices = [];
|
$task_indices = [];
|
||||||
|
|
||||||
// count answers
|
// count answers
|
||||||
$aggregated = [];
|
$aggregated = [];
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
foreach ($entry["challenges"] as $challenge) {
|
foreach ($entry["challenges"] as $task) {
|
||||||
$correct_answer = $challenge["answers"][$challenge["correct_answer"]];
|
$correct_answer = $task["answers"][$task["correct_answer"]];
|
||||||
$compound = $challenge["question"] . $correct_answer . count($challenge["answers"]) . $challenge["image_url"];
|
$compound = $task["question"] . $correct_answer . count($task["answers"]) . $task["image_url"];
|
||||||
$idhash = md5($compound);
|
$idhash = md5($compound);
|
||||||
|
|
||||||
// if this is a new challenge to the list...
|
// if this is a new task to the list...
|
||||||
if (!isset($challenge_indices[$idhash])) {
|
if (!isset($task_indices[$idhash])) {
|
||||||
$challenge_indices[$idhash] = count($challenge_indices);
|
$task_indices[$idhash] = count($task_indices);
|
||||||
$challenge_info = [ // copy challenge info
|
$task_info = [ // copy challenge info
|
||||||
"hash" => $idhash,
|
"hash" => $idhash,
|
||||||
"image_url" => $challenge["image_url"],
|
"image_url" => $task["image_url"],
|
||||||
"question" => $challenge["question"],
|
"question" => $task["question"],
|
||||||
"answers" => $challenge["answers"],
|
"answers" => $task["answers"],
|
||||||
"correct_answer" => $correct_answer,
|
"correct_answer" => $correct_answer,
|
||||||
"player_answers" => array_fill(0, count($challenge["answers"]), 0),
|
"player_answers" => array_fill(0, count($task["answers"]), 0),
|
||||||
"answer_count" => count($challenge["answers"]),
|
"answer_count" => count($task["answers"]),
|
||||||
"skipped" => 0
|
"skipped" => 0
|
||||||
];
|
];
|
||||||
$aggregated[$challenge_indices[$idhash]] = $challenge_info; // insert challenge info
|
$aggregated[$task_indices[$idhash]] = $task_info; // insert task info
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch challenge index
|
// fetch task index
|
||||||
$challenge_idx = $challenge_indices[$idhash];
|
$task_idx = $task_indices[$idhash];
|
||||||
|
|
||||||
// add up player answer
|
// add up player answer
|
||||||
$player_answer = trim($challenge["player_answer"]);
|
$player_answer = trim($task["player_answer"]);
|
||||||
if (($player_answer !== "") && ($player_answer != -1)) { // player answered
|
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
|
$answer_idx = array_search($task["answers"][$task["player_answer"]], $aggregated[$task_idx]["answers"]); // transform player answer index to report answer index
|
||||||
$aggregated[$challenge_idx]["player_answers"][(int)$answer_idx]++;
|
$aggregated[$task_idx]["player_answers"][(int)$answer_idx]++;
|
||||||
} else { // player has not answered or provided an unprocessable answer
|
} else { // player has not answered or provided an unprocessable answer
|
||||||
$aggregated[$challenge_idx]["skipped"]++;
|
$aggregated[$task_idx]["skipped"]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -786,7 +259,7 @@ class TestMgr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// match challenges
|
// match tasks
|
||||||
return $aggregated;
|
return $aggregated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -811,7 +284,7 @@ class TestMgr
|
|||||||
{
|
{
|
||||||
$query = [["time_limited", "=", true], "AND", ["end_limit_time", "<", time()]];
|
$query = [["time_limited", "=", true], "AND", ["end_limit_time", "<", time()]];
|
||||||
if ($ongoingOnly) {
|
if ($ongoingOnly) {
|
||||||
$query = [...$query, "AND", ["state", "=", TEST_ONGOING]];
|
$query = [...$query, "AND", ["state", "=", Test::TEST_ONGOING]];
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb = $this->db->createQueryBuilder();
|
$qb = $this->db->createQueryBuilder();
|
||||||
|
|||||||
65
class/TestSummary.php
Normal file
65
class/TestSummary.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class TestSummary
|
||||||
|
{
|
||||||
|
public int $maxMark; // Maximum mark
|
||||||
|
public int $mark; // Collected mark
|
||||||
|
private float $percentage; // Ratio of correct answers
|
||||||
|
|
||||||
|
// Calculate percentage.
|
||||||
|
private function calculatePercentage(): void
|
||||||
|
{
|
||||||
|
if ($this->maxMark > 0) {
|
||||||
|
$this->percentage = $this->mark / (double)$this->maxMark * 100.0;
|
||||||
|
} else { // avoid division by zero
|
||||||
|
$this->percentage = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __construct(int $taskN, int $correctAnswerN)
|
||||||
|
{
|
||||||
|
$this->maxMark = $taskN;
|
||||||
|
$this->mark = $correctAnswerN;
|
||||||
|
$this->calculatePercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get max mark.
|
||||||
|
function getMaxMark(): int
|
||||||
|
{
|
||||||
|
return $this->maxMark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mark.
|
||||||
|
function getMark(): int
|
||||||
|
{
|
||||||
|
return $this->mark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMark(int $mark): void
|
||||||
|
{
|
||||||
|
$this->mark = $mark;
|
||||||
|
$this->calculatePercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ratio of correct results.
|
||||||
|
function getPercentage(): float
|
||||||
|
{
|
||||||
|
return ($this->mark * 100.0) / $this->maxMark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build from array.
|
||||||
|
static function fromArray(array $a): TestSummary
|
||||||
|
{
|
||||||
|
if (!isset($a["max_mark"]) || !isset($a["mark"])) { // backward compatibility
|
||||||
|
return new TestSummary($a["challenge_n"], $a["correct_answer_n"]);
|
||||||
|
} else {
|
||||||
|
return new TestSummary($a["max_mark"], $a["mark"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to array.
|
||||||
|
function toArray(): array
|
||||||
|
{
|
||||||
|
return ["challenge_n" => $this->maxMark, "correct_answer_n" => $this->mark, "percentage" => $this->percentage];
|
||||||
|
}
|
||||||
|
}
|
||||||
134
class/User.php
Normal file
134
class/User.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once "AutoStoring.php";
|
||||||
|
|
||||||
|
class User extends AutoStoring
|
||||||
|
{
|
||||||
|
private int $id; // User's ID
|
||||||
|
private string $nickname; // User's nickname
|
||||||
|
private string $password; // User's password in it's encoded form or left empty
|
||||||
|
private string $realname; // User's real name displayed in their profile
|
||||||
|
// private array $groups; // User's assigned groups
|
||||||
|
private string $privilege; // User's privilege
|
||||||
|
private UserMgr $userMgr; // UserManager object governing this object.
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
|
||||||
|
// Store modifications to the database.
|
||||||
|
public function storeMods(): void
|
||||||
|
{
|
||||||
|
$this->userMgr->updateUser($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
|
||||||
|
function __construct(UserMgr &$usrmgr, int $id, string $nickname = null, string $password = null, string $realname = null, string $privilege = null)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->id = $id;
|
||||||
|
$this->nickname = $nickname;
|
||||||
|
$this->password = $password;
|
||||||
|
$this->realname = $realname;
|
||||||
|
// $this->groups = $groups;
|
||||||
|
$this->privilege = $privilege;
|
||||||
|
|
||||||
|
// save reference to user manager
|
||||||
|
$this->userMgr = &$usrmgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user from an array
|
||||||
|
static function fromArray(UserMgr &$usrmgr, array $a): User
|
||||||
|
{
|
||||||
|
$id = $a["_id"] ?? -1;
|
||||||
|
return new User($usrmgr, $id, $a["nickname"], $a["password"], $a["realname"], $a["privilege"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert user to array
|
||||||
|
function toArray(array $omit = []): array
|
||||||
|
{
|
||||||
|
$a = [
|
||||||
|
"_id" => $this->id,
|
||||||
|
"nickname" => $this->nickname,
|
||||||
|
"password" => $this->password,
|
||||||
|
"realname" => $this->realname,
|
||||||
|
// "groups" => $this->groups,
|
||||||
|
"privilege" => $this->privilege
|
||||||
|
];
|
||||||
|
|
||||||
|
// omit specific fields
|
||||||
|
foreach ($omit as $field) {
|
||||||
|
unset($a[$field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change user password. If $safe, then $old is checked.
|
||||||
|
function changePassword(string $new, string $old, bool $safe = true): bool
|
||||||
|
{
|
||||||
|
if (!$safe || password_verify($old, $this->password)) {
|
||||||
|
$this->password = password_hash($new, PASSWORD_DEFAULT);
|
||||||
|
$this->storeMods(); // store modifications
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Change user groups
|
||||||
|
// function changeGroups(array $add, array $remove): void
|
||||||
|
// {
|
||||||
|
// alter_array_contents($this->groups, $add, $remove);
|
||||||
|
// $this->storeMods(); // store modifications
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get user's groups
|
||||||
|
// function getGroups(): array
|
||||||
|
// {
|
||||||
|
// return $this->groups;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Set user privilege level
|
||||||
|
function setPrivilege(string $privilege): void
|
||||||
|
{
|
||||||
|
$this->privilege = ($this->nickname === QUIZMASTER_NICKNAME) ? PRIVILEGE_QUIZMASTER : $privilege; // quizmaster's privilege mustn't be tampered with
|
||||||
|
$this->storeMods(); // store modifications
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user privilege level
|
||||||
|
function getPrivilege(): string
|
||||||
|
{
|
||||||
|
return $this->privilege;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's nickname.
|
||||||
|
function getNickname(): string
|
||||||
|
{
|
||||||
|
return $this->nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user's real name.
|
||||||
|
function setRealname(string $realname): void
|
||||||
|
{
|
||||||
|
$this->realname = $realname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's real name.
|
||||||
|
function getRealname(): string
|
||||||
|
{
|
||||||
|
return $this->realname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check against user credentials.
|
||||||
|
function checkPassword(string $password): bool
|
||||||
|
{
|
||||||
|
return password_verify($password, $this->password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has the user quizmaster privileges?
|
||||||
|
function hasQuizmasterPrivilege(): bool
|
||||||
|
{
|
||||||
|
return $this->privilege == PRIVILEGE_QUIZMASTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,136 +8,7 @@ require_once "AutoStoring.php";
|
|||||||
|
|
||||||
require_once "privilege_levels.php";
|
require_once "privilege_levels.php";
|
||||||
|
|
||||||
class User extends AutoStoring
|
require_once "User.php";
|
||||||
{
|
|
||||||
private int $id; // User's ID
|
|
||||||
private string $nickname; // User's nickname
|
|
||||||
private string $password; // User's password in it's encoded form or left empty
|
|
||||||
private string $realname; // User's real name displayed in their profile
|
|
||||||
// private array $groups; // User's assigned groups
|
|
||||||
private string $privilege; // User's privilege
|
|
||||||
private UserMgr $userMgr; // UserManager object governing this object.
|
|
||||||
|
|
||||||
// -------------------------------------------
|
|
||||||
|
|
||||||
// Store modifications to the database.
|
|
||||||
public function storeMods(): void
|
|
||||||
{
|
|
||||||
$this->userMgr->updateUser($this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------
|
|
||||||
|
|
||||||
function __construct(UserMgr &$usrmgr, int $id, string $nickname = null, string $password = null, string $realname = null, string $privilege = null)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
|
|
||||||
$this->id = $id;
|
|
||||||
$this->nickname = $nickname;
|
|
||||||
$this->password = $password;
|
|
||||||
$this->realname = $realname;
|
|
||||||
// $this->groups = $groups;
|
|
||||||
$this->privilege = $privilege;
|
|
||||||
|
|
||||||
// save reference to user manager
|
|
||||||
$this->userMgr = &$usrmgr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user from an array
|
|
||||||
static function fromArray(UserMgr &$usrmgr, array $a): User
|
|
||||||
{
|
|
||||||
$id = $a["_id"] ?? -1;
|
|
||||||
return new User($usrmgr, $id, $a["nickname"], $a["password"], $a["realname"], $a["privilege"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert user to array
|
|
||||||
function toArray(array $omit = []): array
|
|
||||||
{
|
|
||||||
$a = [
|
|
||||||
"_id" => $this->id,
|
|
||||||
"nickname" => $this->nickname,
|
|
||||||
"password" => $this->password,
|
|
||||||
"realname" => $this->realname,
|
|
||||||
// "groups" => $this->groups,
|
|
||||||
"privilege" => $this->privilege
|
|
||||||
];
|
|
||||||
|
|
||||||
// omit specific fields
|
|
||||||
foreach ($omit as $field) {
|
|
||||||
unset($a[$field]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $a;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change user password. If $safe, then $old is checked.
|
|
||||||
function changePassword(string $new, string $old, bool $safe = true): bool
|
|
||||||
{
|
|
||||||
if (!$safe || password_verify($old, $this->password)) {
|
|
||||||
$this->password = password_hash($new, PASSWORD_DEFAULT);
|
|
||||||
$this->storeMods(); // store modifications
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Change user groups
|
|
||||||
// function changeGroups(array $add, array $remove): void
|
|
||||||
// {
|
|
||||||
// alter_array_contents($this->groups, $add, $remove);
|
|
||||||
// $this->storeMods(); // store modifications
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Get user's groups
|
|
||||||
// function getGroups(): array
|
|
||||||
// {
|
|
||||||
// return $this->groups;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Set user privilege level
|
|
||||||
function setPrivilege(string $privilege): void
|
|
||||||
{
|
|
||||||
$this->privilege = ($this->nickname === QUIZMASTER_NICKNAME) ? PRIVILEGE_QUIZMASTER : $privilege; // quizmaster's privilege mustn't be tampered with
|
|
||||||
$this->storeMods(); // store modifications
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user privilege level
|
|
||||||
function getPrivilege(): string
|
|
||||||
{
|
|
||||||
return $this->privilege;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user's nickname.
|
|
||||||
function getNickname(): string
|
|
||||||
{
|
|
||||||
return $this->nickname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set user's real name.
|
|
||||||
function setRealname(string $realname): void
|
|
||||||
{
|
|
||||||
$this->realname = $realname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user's real name.
|
|
||||||
function getRealname(): string
|
|
||||||
{
|
|
||||||
return $this->realname;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check against user credentials.
|
|
||||||
function checkPassword(string $password): bool
|
|
||||||
{
|
|
||||||
return password_verify($password, $this->password);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Has the user quizmaster privileges?
|
|
||||||
function hasQuizmasterPrivilege(): bool
|
|
||||||
{
|
|
||||||
return $this->privilege == PRIVILEGE_QUIZMASTER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserMgr
|
class UserMgr
|
||||||
{
|
{
|
||||||
|
|||||||
9
class/VerilogUtils.php
Normal file
9
class/VerilogUtils.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class VerilogUtils
|
||||||
|
{
|
||||||
|
public static function genTruthTable(): string {
|
||||||
|
// collect input variables
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once "vendor/autoload.php";
|
||||||
|
|
||||||
require_once "class/TestMgr.php";
|
require_once "class/TestMgr.php";
|
||||||
|
require_once "class/GameMgr.php";
|
||||||
|
|
||||||
|
require_once "class/LogicFunction.php";
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
|
||||||
const longopts = [
|
const longopts = [
|
||||||
"action:", // execute some CLI action
|
"action:", // execute some CLI action
|
||||||
@ -22,12 +29,28 @@ if (isset($options["action"])) {
|
|||||||
printf("OK!\n");
|
printf("OK!\n");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "upgrade_games":
|
||||||
|
{
|
||||||
|
printf("Upgrading games...");
|
||||||
|
$gameMgr = new GameMgr();
|
||||||
|
$gameMgr->upgradeGames();
|
||||||
|
printf("OK!\n");
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "get_timed_tests":
|
case "get_timed_tests":
|
||||||
{
|
{
|
||||||
$testMgr = new TestMgr();
|
$testMgr = new TestMgr();
|
||||||
printf("Expired timed tests: %s\n", join(", ", $testMgr->extractExpiredTimedTestIds()));
|
printf("Expired timed tests: %s\n", join(", ", $testMgr->extractExpiredTimedTestIds()));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "gen_random":
|
||||||
|
{
|
||||||
|
//$lf = LogicFunction::genRandom(["a", "b", "c"], 2, 4);
|
||||||
|
$lf = LogicFunction::genRandomDF(["a", "b", "c"]);
|
||||||
|
printf("Verilog-form: %s\nTeX-form: %s\n", $lf->verilog_form, $lf->tex_form);
|
||||||
|
$lf->getTruthTable();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
"ext-http": "*",
|
"ext-http": "*",
|
||||||
"ext-mbstring" : "*",
|
"ext-mbstring" : "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"ext-fileinfo": "*"
|
"ext-fileinfo": "*",
|
||||||
|
"phpoffice/phpspreadsheet": "^5.1",
|
||||||
|
"symfony/expression-language": "^7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,10 +71,10 @@ if (!get_autologin_state() || (($user_data["privilege"] !== PRIVILEGE_CREATOR) &
|
|||||||
<tr>
|
<tr>
|
||||||
<td><label>Kérdés-fájlok:</label></td>
|
<td><label>Kérdés-fájlok:</label></td>
|
||||||
<td>
|
<td>
|
||||||
<input type="button" id="download_challenges_btn" value="Letöltés CSV-ként" shown="false"
|
<input type="button" id="download_tasks_btn" value="Letöltés CSV-ként" shown="false"
|
||||||
onclick="download_challenges()">
|
onclick="download_tasks()">
|
||||||
<input type="button" id="edit_challenges_btn" value="Szerkesztés"
|
<input type="button" id="edit_tasks_btn" value="Szerkesztés"
|
||||||
onclick="edit_challenges()" shown="false">
|
onclick="edit_tasks()" shown="false">
|
||||||
<input type="button" value="Új feltöltése" id="show_game_file_upload"
|
<input type="button" value="Új feltöltése" id="show_game_file_upload"
|
||||||
onclick="show_hide_gamefile_upload(true)">
|
onclick="show_hide_gamefile_upload(true)">
|
||||||
<input type="file" id="game_file" shown="false">
|
<input type="file" id="game_file" shown="false">
|
||||||
|
|||||||
114
interface.php
114
interface.php
@ -32,6 +32,10 @@ require_once "class/TestMgr.php";
|
|||||||
|
|
||||||
require_once "class/ReportBuilder.php";
|
require_once "class/ReportBuilder.php";
|
||||||
|
|
||||||
|
require_once "vendor/autoload.php";
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
|
|
||||||
$userMgr = new UserMgr();
|
$userMgr = new UserMgr();
|
||||||
@ -72,7 +76,9 @@ function login(ReqHandler &$rh, array $params): string
|
|||||||
|
|
||||||
$user = $userMgr->getUser($nickname);
|
$user = $userMgr->getUser($nickname);
|
||||||
if (($user !== null) && $user->checkPassword($password)) {
|
if (($user !== null) && $user->checkPassword($password)) {
|
||||||
session_start();
|
if (session_status() == PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
$_SESSION["nickname"] = $nickname;
|
$_SESSION["nickname"] = $nickname;
|
||||||
$result = "OK";
|
$result = "OK";
|
||||||
} else {
|
} else {
|
||||||
@ -152,7 +158,11 @@ function start_or_continue_test(ReqHandler &$rh, array $params): string
|
|||||||
if ($groupMgr->doesUserAccessGame($params["gameid"], $user->getNickname())) {
|
if ($groupMgr->doesUserAccessGame($params["gameid"], $user->getNickname())) {
|
||||||
$game = $gameMgr->getGame($params["gameid"]);
|
$game = $gameMgr->getGame($params["gameid"]);
|
||||||
$test = $testMgr->addOrContinueTest($game, $user);
|
$test = $testMgr->addOrContinueTest($game, $user);
|
||||||
return $test->getId();
|
if ($test != null) {
|
||||||
|
return $test->getId();
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -235,10 +245,10 @@ function access_test_data(string $testid): Test|null
|
|||||||
return $test;
|
return $test;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exclude_correct_answers(array &$challenges): void
|
function exclude_correct_answers(array &$tasks): void
|
||||||
{
|
{
|
||||||
foreach ($challenges as &$challenge) {
|
foreach ($tasks as &$task) {
|
||||||
$challenge["correct_answer"] = -1;
|
$task["correct_answer"] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,7 +273,7 @@ function save_player_answer(ReqHandler &$rh, array $params): string
|
|||||||
{
|
{
|
||||||
$test = access_test_data($params["testid"]);
|
$test = access_test_data($params["testid"]);
|
||||||
if ($test !== null) {
|
if ($test !== null) {
|
||||||
$test->saveAnswer($params["challenge_index"], $params["answer_index"]);
|
$test->saveAnswer($params["task_index"], $params["answer"]);
|
||||||
return "OK";
|
return "OK";
|
||||||
} else {
|
} else {
|
||||||
return "FAIL";
|
return "FAIL";
|
||||||
@ -310,7 +320,7 @@ function get_image(ReqHandler &$rh, array $params): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rh->add("get_test", ["testid"], PRIVILEGE_PLAYER, "get_player_test", RESP_JSON, "Get player's test by ID.");
|
$rh->add("get_test", ["testid"], PRIVILEGE_PLAYER, "get_player_test", RESP_JSON, "Get player's test by ID.");
|
||||||
$rh->add("save_answer", ["testid", "challenge_index", "answer_index"], PRIVILEGE_PLAYER, "save_player_answer", RESP_PLAIN, "Store player's answer.");
|
$rh->add("save_answer", ["testid", "task_index", "answer"], PRIVILEGE_PLAYER, "save_player_answer", RESP_PLAIN, "Store player's answer.");
|
||||||
$rh->add("submit_test", ["testid"], PRIVILEGE_PLAYER, "submit_test", RESP_PLAIN, "Finish player's test.");
|
$rh->add("submit_test", ["testid"], PRIVILEGE_PLAYER, "submit_test", RESP_PLAIN, "Finish player's test.");
|
||||||
$rh->add("get_image", ["gameid", "img_url"], PRIVILEGE_PLAYER, "get_image", RESP_NONE, "Get image per game.");
|
$rh->add("get_image", ["gameid", "img_url"], PRIVILEGE_PLAYER, "get_image", RESP_NONE, "Get image per game.");
|
||||||
|
|
||||||
@ -378,39 +388,59 @@ function create_update_game(ReqHandler &$rh, array $params): array
|
|||||||
|
|
||||||
// update game file if supplied
|
// update game file if supplied
|
||||||
if (isset($_FILES["game_file"])) {
|
if (isset($_FILES["game_file"])) {
|
||||||
// decide weather it's a package or a plain table
|
// decide whether it's a package or a plain table
|
||||||
$file = $_FILES["game_file"];
|
$file = $_FILES["game_file"];
|
||||||
$challenge_import_status = [];
|
$task_import_status = ["n" => 0, "encoding" => "(N/A)"];
|
||||||
|
|
||||||
|
// fetch actual and temporary file name
|
||||||
|
$file_name = $file["name"];
|
||||||
|
$file_path = $file["tmp_name"];
|
||||||
|
|
||||||
// determine MIME type
|
// determine MIME type
|
||||||
$file_type = strtolower(pathinfo($file["name"], PATHINFO_EXTENSION));
|
$process_once_more = false;
|
||||||
|
do {
|
||||||
|
// don't process one more time by default
|
||||||
|
$process_once_more = false;
|
||||||
|
|
||||||
if ($file_type === "zip") { // a package was uploaded
|
// get file type
|
||||||
$zip = new ZipArchive;
|
$file_type = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||||
if ($zip->open($file["tmp_name"])) {
|
|
||||||
|
|
||||||
$game_dir = $game->getGameDir(); // get game directory
|
// extract the archive if a ZIP package was uploaded
|
||||||
//$game_files = glob($game_dir); // get list of existing game files
|
if ($file_type === "zip") {
|
||||||
// remove former files recursively
|
$zip = new ZipArchive;
|
||||||
$dir_iter = new RecursiveDirectoryIterator($game_dir, FilesystemIterator::SKIP_DOTS);
|
if ($zip->open($file_path)) {
|
||||||
foreach ($dir_iter as $dir_item) {
|
// get game directory
|
||||||
$item_path = $dir_item->getPathname();
|
$game_dir = $game->getGameDir();
|
||||||
$dir_item->isDir() ? rmdir($item_path) : unlink($item_path);
|
//$game_files = glob($game_dir); // get list of existing game files
|
||||||
}
|
|
||||||
|
// remove former files recursively
|
||||||
// extract package contents to the game directory
|
$dir_iter = new RecursiveDirectoryIterator($game_dir, FilesystemIterator::SKIP_DOTS);
|
||||||
$zip->extractTo($game_dir . DIRECTORY_SEPARATOR);
|
foreach ($dir_iter as $dir_item) {
|
||||||
|
$item_path = $dir_item->getPathname();
|
||||||
// search for the CSV table file
|
$dir_item->isDir() ? rmdir($item_path) : unlink($item_path);
|
||||||
$csv_files = glob($game_dir . DIRECTORY_SEPARATOR . "*.csv") ?? [];
|
}
|
||||||
if (count($csv_files) > 0) {
|
|
||||||
$challenge_import_status = $game->importChallengesFromCSV($csv_files[0]);
|
// extract package contents to the game directory
|
||||||
|
$zip->extractTo($game_dir . DIRECTORY_SEPARATOR);
|
||||||
|
|
||||||
|
// search for the table file
|
||||||
|
$table_files = glob($game_dir . DIRECTORY_SEPARATOR . "*.{csv,xls,xlsx,ods}", \GLOB_BRACE);
|
||||||
|
if (count($table_files) > 0) {
|
||||||
|
$file_name = $table_files[0];
|
||||||
|
$file_path = $file_name;
|
||||||
|
$process_once_more = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if ($file_type === "csv") { // a plain table was uploaded
|
||||||
|
$task_import_status = $game->importTasksFromCSV($file_path);
|
||||||
|
} else if (in_array($file_type, ["xls", "xlsx", "ods"])) {
|
||||||
|
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_path, readers: [ucfirst($file_type)]);
|
||||||
|
$sheet = $spreadsheet->getSheet(0);
|
||||||
|
$table = $sheet->toArray();
|
||||||
|
$task_import_status = $game->importTasksFromTable($table);
|
||||||
}
|
}
|
||||||
} else if ($file_type === "csv") { // a plain table was uploaded
|
} while ($process_once_more);
|
||||||
$challenge_import_status = $game->importChallengesFromCSV($file["tmp_name"]);
|
$result = $task_import_status;
|
||||||
}
|
|
||||||
$result = $challenge_import_status;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -430,7 +460,7 @@ function get_all_game_headers(ReqHandler &$rh, array $params): array
|
|||||||
return $a;
|
return $a;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_challenges(ReqHandler &$rh, array $params): string
|
function get_tasks(ReqHandler &$rh, array $params): string
|
||||||
{
|
{
|
||||||
global $user;
|
global $user;
|
||||||
global $gameMgr;
|
global $gameMgr;
|
||||||
@ -473,7 +503,7 @@ function export_game_file_csv(ReqHandler &$rh, array $params): string
|
|||||||
$f = tmpfile();
|
$f = tmpfile();
|
||||||
header("Content-Type: text/csv");
|
header("Content-Type: text/csv");
|
||||||
header("Content-Disposition: attachment; filename=\"challenges_$gameid.csv\"\r\n");
|
header("Content-Disposition: attachment; filename=\"challenges_$gameid.csv\"\r\n");
|
||||||
$game->exportChallengesToCSV($f);
|
$game->exportTasksToCSV($f);
|
||||||
fseek($f, 0);
|
fseek($f, 0);
|
||||||
fpassthru($f);
|
fpassthru($f);
|
||||||
}
|
}
|
||||||
@ -653,7 +683,7 @@ function delete_tests(ReqHandler &$rh, array $params): string
|
|||||||
|
|
||||||
$rh->add(["create_game", "update_game"], ["data"], PRIVILEGE_CREATOR, "create_update_game", RESP_JSON, "Create or update game.");
|
$rh->add(["create_game", "update_game"], ["data"], PRIVILEGE_CREATOR, "create_update_game", RESP_JSON, "Create or update game.");
|
||||||
$rh->add("get_all_game_headers", [], PRIVILEGE_CREATOR, "get_all_game_headers", RESP_JSON, "Get all game headers.");
|
$rh->add("get_all_game_headers", [], PRIVILEGE_CREATOR, "get_all_game_headers", RESP_JSON, "Get all game headers.");
|
||||||
$rh->add("get_challenges", [], PRIVILEGE_CREATOR, "get_challenges", RESP_PLAIN, "Get game challenges.");
|
$rh->add("get_tasks", [], PRIVILEGE_CREATOR, "get_tasks", RESP_PLAIN, "Get game tasks.");
|
||||||
$rh->add("delete_games", ["ids"], PRIVILEGE_CREATOR, "delete_games", RESP_PLAIN, "Delete games.");
|
$rh->add("delete_games", ["ids"], PRIVILEGE_CREATOR, "delete_games", RESP_PLAIN, "Delete games.");
|
||||||
$rh->add("export_game_file_csv", ["gameid"], PRIVILEGE_CREATOR, "export_game_file_csv", RESP_NONE, "Export game CSV file.");
|
$rh->add("export_game_file_csv", ["gameid"], PRIVILEGE_CREATOR, "export_game_file_csv", RESP_NONE, "Export game CSV file.");
|
||||||
$rh->add("get_results_by_gameid", ["gameid"], PRIVILEGE_CREATOR, "get_results_by_gameid", RESP_JSON, "Get game results.");
|
$rh->add("get_results_by_gameid", ["gameid"], PRIVILEGE_CREATOR, "get_results_by_gameid", RESP_JSON, "Get game results.");
|
||||||
@ -864,6 +894,16 @@ function import_users_from_csv(ReqHandler &$rh, array $params): string
|
|||||||
return "OK";
|
return "OK";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function execute_cli_command(ReqHandler &$rh, array $params): string
|
||||||
|
{
|
||||||
|
$args = $params["cmd"];
|
||||||
|
$cmdline = "php cli_actions.php $args";
|
||||||
|
$resp = "=> " . $cmdline . "\n\n";
|
||||||
|
$resp .= shell_exec($cmdline);
|
||||||
|
//$resp = shell_exec("php -v");
|
||||||
|
return $resp ?? "(null output)";
|
||||||
|
}
|
||||||
|
|
||||||
$rh->add("create_group", ["groupname", "description"], PRIVILEGE_QUIZMASTER, "create_update_group", RESP_PLAIN, "Create group.");
|
$rh->add("create_group", ["groupname", "description"], PRIVILEGE_QUIZMASTER, "create_update_group", RESP_PLAIN, "Create group.");
|
||||||
$rh->add("update_group", ["groupname", "description", "owner", "editors", "id"], PRIVILEGE_QUIZMASTER, "create_update_group", RESP_PLAIN, "Update group.");
|
$rh->add("update_group", ["groupname", "description", "owner", "editors", "id"], PRIVILEGE_QUIZMASTER, "create_update_group", RESP_PLAIN, "Update group.");
|
||||||
$rh->add("delete_groups", ["ids"], PRIVILEGE_QUIZMASTER, "delete_groups", RESP_PLAIN, "Delete group.");
|
$rh->add("delete_groups", ["ids"], PRIVILEGE_QUIZMASTER, "delete_groups", RESP_PLAIN, "Delete group.");
|
||||||
@ -878,6 +918,8 @@ $rh->add("get_user_groups", ["nickname"], PRIVILEGE_QUIZMASTER, "get_user_groups
|
|||||||
$rh->add("get_game_groups", ["gameid"], PRIVILEGE_QUIZMASTER, "get_game_groups", RESP_JSON, "Get game's groups.");
|
$rh->add("get_game_groups", ["gameid"], PRIVILEGE_QUIZMASTER, "get_game_groups", RESP_JSON, "Get game's groups.");
|
||||||
$rh->add("import_users_from_csv", [], PRIVILEGE_QUIZMASTER, "import_users_from_csv", RESP_JSON, "Get all users.");
|
$rh->add("import_users_from_csv", [], PRIVILEGE_QUIZMASTER, "import_users_from_csv", RESP_JSON, "Get all users.");
|
||||||
|
|
||||||
|
$rh->add("execute_cli_command", ["cmd"], PRIVILEGE_QUIZMASTER, "execute_cli_command", RESP_PLAIN, "Run cli command.");
|
||||||
|
|
||||||
//function test(ReqHandler &$rh, array $params): string
|
//function test(ReqHandler &$rh, array $params): string
|
||||||
//{
|
//{
|
||||||
// $usrmgr = new UserMgr();
|
// $usrmgr = new UserMgr();
|
||||||
|
|||||||
@ -56,7 +56,11 @@ function start_or_continue_test(gameid) {
|
|||||||
request(req).then(resp => {
|
request(req).then(resp => {
|
||||||
if (resp.length > 0) // response is non-zero
|
if (resp.length > 0) // response is non-zero
|
||||||
{
|
{
|
||||||
open_test(resp, gameid);
|
if (Number(resp) !== -1) {
|
||||||
|
open_test(resp, gameid);
|
||||||
|
} else {
|
||||||
|
alert("A teszt nem indítható el!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,10 +59,10 @@ function create_edit_game(game = null) {
|
|||||||
let ownerF = document.getElementById("game_owner");
|
let ownerF = document.getElementById("game_owner");
|
||||||
let contributorsF = document.getElementById("game_contributors");
|
let contributorsF = document.getElementById("game_contributors");
|
||||||
let gameFileF = document.getElementById("game_file");
|
let gameFileF = document.getElementById("game_file");
|
||||||
let download_challenges_btn = document.getElementById("download_challenges_btn");
|
let download_tasks_btn = document.getElementById("download_tasks_btn");
|
||||||
let show_game_file_upload_btn = document.getElementById("show_game_file_upload");
|
let show_game_file_upload_btn = document.getElementById("show_game_file_upload");
|
||||||
let cancel_game_file_upload_btn = document.getElementById("cancel_game_file_upload");
|
let cancel_game_file_upload_btn = document.getElementById("cancel_game_file_upload");
|
||||||
let edit_challenges_btn = document.getElementById("edit_challenges_btn");
|
let edit_tasks_btn = document.getElementById("edit_tasks_btn");
|
||||||
let groupF = document.getElementById("game_groups");
|
let groupF = document.getElementById("game_groups");
|
||||||
let time_limitedChk = document.getElementById("time_limited");
|
let time_limitedChk = document.getElementById("time_limited");
|
||||||
let time_limitF = document.getElementById("time_limit");
|
let time_limitF = document.getElementById("time_limit");
|
||||||
@ -136,14 +136,14 @@ function create_edit_game(game = null) {
|
|||||||
|
|
||||||
let game_file_present = updating && game["game_file_present"];
|
let game_file_present = updating && game["game_file_present"];
|
||||||
if (game_file_present) {
|
if (game_file_present) {
|
||||||
show(download_challenges_btn);
|
show(download_tasks_btn);
|
||||||
show(edit_challenges_btn);
|
show(edit_tasks_btn);
|
||||||
edit_challenges_btn.onclick = () => {
|
edit_tasks_btn.onclick = () => {
|
||||||
edit_challenges(game);
|
edit_tasks(game);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
hide(download_challenges_btn);
|
hide(download_tasks_btn);
|
||||||
hide(edit_challenges_btn);
|
hide(edit_tasks_btn);
|
||||||
}
|
}
|
||||||
show_hide_gamefile_upload(false);
|
show_hide_gamefile_upload(false);
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ function show_hide_gamefile_upload(en) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function download_challenges() {
|
function download_tasks() {
|
||||||
let action = "export_game_file_csv";
|
let action = "export_game_file_csv";
|
||||||
let gameid = EDITED_GAME["_id"];
|
let gameid = EDITED_GAME["_id"];
|
||||||
window.open(`interface.php?action=${action}&gameid=${gameid}`, "_blank");
|
window.open(`interface.php?action=${action}&gameid=${gameid}`, "_blank");
|
||||||
@ -251,8 +251,8 @@ function handle_time_limit_chkbox() {
|
|||||||
time_limitF.disabled = !time_limitedChk.checked;
|
time_limitF.disabled = !time_limitedChk.checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit_challenges(game) {
|
function edit_tasks(game) {
|
||||||
let req = {action: "get_challenges", gameid: game["_id"]};
|
let req = {action: "get_tasks", gameid: game["_id"]};
|
||||||
request(req).then(resp => {
|
request(req).then(resp => {
|
||||||
console.log(JSON.parse(resp));
|
console.log(JSON.parse(resp));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -158,35 +158,35 @@ function generate_report() {
|
|||||||
report_display.innerHTML = "";
|
report_display.innerHTML = "";
|
||||||
|
|
||||||
let ch_n = 0;
|
let ch_n = 0;
|
||||||
stats.forEach((challenge) => {
|
stats.forEach((task) => {
|
||||||
let challenge_box = document.createElement("section");
|
let task_box = document.createElement("section");
|
||||||
challenge_box.classList.add("challenge");
|
task_box.classList.add("task");
|
||||||
challenge_box.style.width = "100%";
|
task_box.style.width = "100%";
|
||||||
|
|
||||||
let seq_num = document.createElement("section");
|
let seq_num = document.createElement("section");
|
||||||
seq_num.classList.add("seq-num");
|
seq_num.classList.add("seq-num");
|
||||||
seq_num.innerText = ++ch_n;
|
seq_num.innerText = ++ch_n;
|
||||||
challenge_box.append(seq_num);
|
task_box.append(seq_num);
|
||||||
|
|
||||||
let img_url = challenge["image_url"];
|
let img_url = task["image_url"];
|
||||||
if (img_url !== "") {
|
if (img_url !== "") {
|
||||||
let fig = document.createElement("img");
|
let fig = document.createElement("img");
|
||||||
fig.src = `interface.php?action=get_image&gameid=${GAMEID}&img_url=${challenge["image_url"]}`;
|
fig.src = `interface.php?action=get_image&gameid=${GAMEID}&img_url=${task["image_url"]}`;
|
||||||
fig.classList.add("question-image");
|
fig.classList.add("question-image");
|
||||||
challenge_box.append(fig);
|
task_box.append(fig);
|
||||||
}
|
}
|
||||||
|
|
||||||
let question = document.createElement("span");
|
let question = document.createElement("span");
|
||||||
question.classList.add("question");
|
question.classList.add("question");
|
||||||
question.innerHTML = preprocess_inserts(challenge["question"]);
|
question.innerHTML = preprocess_inserts(task["question"]);
|
||||||
let answer_container = document.createElement("section");
|
let answer_container = document.createElement("section");
|
||||||
answer_container.classList.add("answer-container");
|
answer_container.classList.add("answer-container");
|
||||||
challenge_box.append(question, answer_container);
|
task_box.append(question, answer_container);
|
||||||
|
|
||||||
let n = challenge["answer_count"];
|
let n = task["answer_count"];
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
let answer = challenge["answers"][i];
|
let answer = task["answers"][i];
|
||||||
let correct_answer = answer === challenge["correct_answer"];
|
let correct_answer = answer === task["correct_answer"];
|
||||||
|
|
||||||
let answer_section = document.createElement("section");
|
let answer_section = document.createElement("section");
|
||||||
answer_section.classList.add("answer");
|
answer_section.classList.add("answer");
|
||||||
@ -196,7 +196,7 @@ function generate_report() {
|
|||||||
let progress_bar_indicator = document.createElement("section");
|
let progress_bar_indicator = document.createElement("section");
|
||||||
progress_bar_indicator.classList.add("pb-indicator")
|
progress_bar_indicator.classList.add("pb-indicator")
|
||||||
|
|
||||||
let percentage = challenge["answer_ratio"][i] * 100;
|
let percentage = task["answer_ratio"][i] * 100;
|
||||||
progress_bar_indicator.style.width = `${percentage}%`;
|
progress_bar_indicator.style.width = `${percentage}%`;
|
||||||
progress_bar_indicator.innerText = Math.round(percentage * 100.0) / 100.0 + "%";
|
progress_bar_indicator.innerText = Math.round(percentage * 100.0) / 100.0 + "%";
|
||||||
progress_bar_indicator.setAttribute("correct", correct_answer ? "true" : "false");
|
progress_bar_indicator.setAttribute("correct", correct_answer ? "true" : "false");
|
||||||
@ -213,7 +213,7 @@ function generate_report() {
|
|||||||
answer_container.append(answer_section);
|
answer_container.append(answer_section);
|
||||||
}
|
}
|
||||||
|
|
||||||
report_display.append(challenge_box);
|
report_display.append(task_box);
|
||||||
});
|
});
|
||||||
|
|
||||||
statsTab.MathJax.typeset();
|
statsTab.MathJax.typeset();
|
||||||
|
|||||||
465
js/tasks.js
Normal file
465
js/tasks.js
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
class Task extends HTMLElement {
|
||||||
|
static sequence_number = 0;
|
||||||
|
constructor(type) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.task_type = type;
|
||||||
|
this.sequence_number = Task.sequence_number++;
|
||||||
|
this.concluded = false;
|
||||||
|
this.view_only = false;
|
||||||
|
this.upload_answer_cb = null;
|
||||||
|
this.player_answer = null;
|
||||||
|
|
||||||
|
this.shadow = this.attachShadow({mode: "open"});
|
||||||
|
this.createStyle();
|
||||||
|
this.createElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
createStyle() {
|
||||||
|
this.css = document.createElement("style");
|
||||||
|
this.css.innerHTML = `
|
||||||
|
span.question {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #176767;
|
||||||
|
}
|
||||||
|
section.task {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 40em;
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #d3e5e5;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
section.seq-num {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #176767;
|
||||||
|
color: whitesmoke;
|
||||||
|
border-bottom-right-radius: 0.3em;
|
||||||
|
border-top-left-radius: 0.3em;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
section.answer-container {
|
||||||
|
/* (empty) */
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
section.task {
|
||||||
|
width: calc(100vw - 3em);
|
||||||
|
}
|
||||||
|
section.answer-container {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
this.shadow.append(this.css);
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
let task_box = document.createElement("section");
|
||||||
|
task_box.classList.add("task");
|
||||||
|
let question_span = document.createElement("span");
|
||||||
|
question_span.classList.add("question");
|
||||||
|
let answer_container = document.createElement("section");
|
||||||
|
answer_container.classList.add("answer-container");
|
||||||
|
let seq_num_section = document.createElement("section");
|
||||||
|
seq_num_section.classList.add("seq-num");
|
||||||
|
seq_num_section.innerText = `${this.sequence_number+1}.`;
|
||||||
|
|
||||||
|
task_box.append(question_span);
|
||||||
|
task_box.append(answer_container);
|
||||||
|
task_box.append(seq_num_section);
|
||||||
|
|
||||||
|
this.shadow.append(task_box);
|
||||||
|
|
||||||
|
this.task_box = task_box;
|
||||||
|
this.question_span = question_span;
|
||||||
|
this.answer_container = answer_container;
|
||||||
|
this.seq_num_section = seq_num_section;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {}
|
||||||
|
|
||||||
|
disconnectedCallback() {}
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return this.task_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
set type(type) {
|
||||||
|
this.task_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sequenceNumber() {
|
||||||
|
return this.sequence_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuestion(question) {
|
||||||
|
this.question_span.innerHTML = preprocess_inserts(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConcluded() {
|
||||||
|
return this.concluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
set isConcluded(concluded) {
|
||||||
|
this.concluded = concluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isViewOnly() {
|
||||||
|
return this.view_only;
|
||||||
|
}
|
||||||
|
|
||||||
|
set isViewOnly(viewOnly) {
|
||||||
|
this.view_only = viewOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCorrect() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set playerAnswer(player_answer) {
|
||||||
|
this.player_answer = player_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get playerAnswer() {
|
||||||
|
return this.player_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
set uploadAnswerCb(cb) {
|
||||||
|
this.upload_answer_cb = cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadAnswer() {
|
||||||
|
if (this.upload_answer_cb !== null) {
|
||||||
|
this.upload_answer_cb(this.sequence_number, this.playerAnswer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fromArray(a) {
|
||||||
|
this.setQuestion(a["question"]);
|
||||||
|
this.playerAnswer = a["player_answer"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PicturedTask extends Task {
|
||||||
|
constructor(type) {
|
||||||
|
super(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
createStyle() {
|
||||||
|
super.createStyle();
|
||||||
|
|
||||||
|
this.css.innerHTML += `
|
||||||
|
img.question-image {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
margin: 1em auto;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
super.createElements();
|
||||||
|
|
||||||
|
this.img = document.createElement("img");
|
||||||
|
this.img.classList.add("question-image");
|
||||||
|
this.img.src = "";
|
||||||
|
|
||||||
|
this.task_box.insertBefore(this.img, this.question_span);
|
||||||
|
}
|
||||||
|
|
||||||
|
set imgUrl(url) {
|
||||||
|
url = url.trim();
|
||||||
|
this.img.src = url.trim();
|
||||||
|
this.img.style.display = (url !== "") ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
get imgUrl() {
|
||||||
|
return this.img.src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SingleChoiceTask extends PicturedTask {
|
||||||
|
constructor() {
|
||||||
|
super("singlechoice");
|
||||||
|
|
||||||
|
this.answers = []
|
||||||
|
this.correct_answer = -1;
|
||||||
|
this.player_answer = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
createStyle() {
|
||||||
|
super.createStyle();
|
||||||
|
|
||||||
|
this.css.innerHTML += `
|
||||||
|
section.answer {
|
||||||
|
margin: 0.3em 0.8em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
section.answer label {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 85%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
section.answer label.correct-answer {
|
||||||
|
border: 2px solid #176767 !important;
|
||||||
|
background-color: #176767;
|
||||||
|
color: whitesmoke;
|
||||||
|
/*padding: 0.1em;*/
|
||||||
|
}
|
||||||
|
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
|
||||||
|
background-color: #176767;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
section.bad-answer {
|
||||||
|
background-color: #e5d8d3;
|
||||||
|
}
|
||||||
|
section.bad-answer section.answer input[type="radio"]:checked+label:not(.correct-answer) {
|
||||||
|
background-color: #aa8a7d;
|
||||||
|
}
|
||||||
|
.MathJax {
|
||||||
|
display: block;
|
||||||
|
margin: 0.5em auto;
|
||||||
|
font-size: 120%;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
section.answer label {
|
||||||
|
max-width: calc(100% - 4em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
super.createElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------
|
||||||
|
|
||||||
|
setAnswers(answers) {
|
||||||
|
this.answers = answers;
|
||||||
|
|
||||||
|
this.answers.forEach((answer, i) => {
|
||||||
|
let answer_section = document.createElement("section");
|
||||||
|
answer_section.classList.add("answer");
|
||||||
|
let answer_radio = document.createElement("input");
|
||||||
|
answer_radio.type = "radio";
|
||||||
|
answer_radio.id = `${this.sequenceNumber}_${i}`;
|
||||||
|
answer_radio.name = `task_${this.sequenceNumber}`;
|
||||||
|
answer_radio.disabled = this.isConcluded || this.isViewOnly;
|
||||||
|
let answer_N_snapshot = i;
|
||||||
|
answer_radio.addEventListener("input", () => {
|
||||||
|
this.playerAnswer = answer_N_snapshot;
|
||||||
|
this.uploadAnswer();
|
||||||
|
});
|
||||||
|
|
||||||
|
let answer_text = document.createElement("label");
|
||||||
|
answer_text.innerHTML = preprocess_inserts(answer);
|
||||||
|
answer_text.htmlFor = answer_radio.id;
|
||||||
|
if (this.isConcluded && (this.correctAnswer === i)) {
|
||||||
|
answer_text.classList.add("correct-answer")
|
||||||
|
|
||||||
|
if (this.playerAnswer !== this.correctAnswer) {
|
||||||
|
this.task_box.classList.add("bad-answer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.playerAnswer === i) {
|
||||||
|
answer_radio.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
answer_section.append(answer_radio, answer_text);
|
||||||
|
this.answer_container.append(answer_section);
|
||||||
|
});
|
||||||
|
|
||||||
|
MathJax.typeset([ this.task_box ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
set correctAnswer(correct_answer) {
|
||||||
|
this.correct_answer = correct_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get correctAnswer() {
|
||||||
|
return this.correct_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isCorrect() {
|
||||||
|
return this.player_answer === this.correct_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromArray(a) {
|
||||||
|
super.fromArray(a);
|
||||||
|
|
||||||
|
this.correctAnswer = a["correct_answer"];
|
||||||
|
this.setAnswers(a["answers"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenEndedTask extends PicturedTask {
|
||||||
|
constructor() {
|
||||||
|
super("openended");
|
||||||
|
}
|
||||||
|
|
||||||
|
createStyle() {
|
||||||
|
super.createStyle();
|
||||||
|
|
||||||
|
this.css.innerHTML += `
|
||||||
|
input[type="text"] {
|
||||||
|
font-family: 'Monaco', monospaced;
|
||||||
|
border-width: 0 0 2.2pt 0;
|
||||||
|
background-color: transparent;
|
||||||
|
width: calc(100% - 4em);
|
||||||
|
margin: 1em 0;
|
||||||
|
border-bottom-color: #176767;
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
input[type="text"]:hover {
|
||||||
|
border-bottom-color: #408d8d;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
super.createElements();
|
||||||
|
|
||||||
|
let answer_tf = document.createElement("input");
|
||||||
|
answer_tf.type = "text";
|
||||||
|
answer_tf.placeholder = "(válasz)";
|
||||||
|
|
||||||
|
answer_tf.onblur = () => {
|
||||||
|
this.uploadAnswer();
|
||||||
|
}
|
||||||
|
answer_tf.oninput = () => {
|
||||||
|
this.player_answer = answer_tf.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.answer_container.append(answer_tf);
|
||||||
|
|
||||||
|
this.answer_tf = answer_tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromArray(a) {
|
||||||
|
super.fromArray(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
set playerAnswer(player_answer) {
|
||||||
|
super.playerAnswer = player_answer;
|
||||||
|
this.answer_tf.value = player_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get playerAnswer() {
|
||||||
|
return this.player_answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnswerFieldState() {
|
||||||
|
this.answer_tf.disabled = this.isViewOnly || this.isConcluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
set isConcluded(concluded) {
|
||||||
|
super.isConcluded = concluded;
|
||||||
|
this.updateAnswerFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConcluded() {
|
||||||
|
return super.isConcluded;
|
||||||
|
}
|
||||||
|
|
||||||
|
set isViewOnly(is_view_only) {
|
||||||
|
super.isViewOnly = is_view_only;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isViewOnly() {
|
||||||
|
return super.isViewOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumberConversionTask extends OpenEndedTask {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.type = "numberconversion";
|
||||||
|
}
|
||||||
|
|
||||||
|
createStyle() {
|
||||||
|
super.createStyle();
|
||||||
|
|
||||||
|
this.css.innerHTML += `
|
||||||
|
input[type="text"] {
|
||||||
|
min-width: 5em;
|
||||||
|
width: unset;
|
||||||
|
}
|
||||||
|
section#src, section#dst {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Monaco', monospace;
|
||||||
|
color: #176767;
|
||||||
|
}
|
||||||
|
section#src {
|
||||||
|
margin-right: 1ch;
|
||||||
|
}
|
||||||
|
sub {
|
||||||
|
position: relative;
|
||||||
|
top: 0.8em;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
super.createElements();
|
||||||
|
|
||||||
|
let src_sec = document.createElement("section");
|
||||||
|
src_sec.id = "src";
|
||||||
|
let dst_sec = document.createElement("section");
|
||||||
|
dst_sec.id = "dst";
|
||||||
|
|
||||||
|
this.answer_container.insertBefore(src_sec, this.answer_tf);
|
||||||
|
this.answer_container.append(dst_sec);
|
||||||
|
|
||||||
|
this.src_sec = src_sec;
|
||||||
|
this.dst_sec = dst_sec;
|
||||||
|
|
||||||
|
this.answer_tf.addEventListener("input", () => {
|
||||||
|
this.updateAnswerFieldLength();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fromArray(a) {
|
||||||
|
super.fromArray(a);
|
||||||
|
|
||||||
|
const regex = /([0-9]+)([suc]):([0-9]+)->([0-9]+)([suc]):([0-9]+)/g;
|
||||||
|
let parts = [ ...a["instruction"].matchAll(regex) ][0];
|
||||||
|
|
||||||
|
let src_exp = `${a["source"]}<sub>(${parts[1]})</sub> =`;
|
||||||
|
let dst_exp = `<sub>(${parts[4]})</sub> <i>(${parts[6]} digiten)</i>`;
|
||||||
|
|
||||||
|
this.src_sec.innerHTML = src_exp;
|
||||||
|
this.dst_sec.innerHTML = dst_exp;
|
||||||
|
|
||||||
|
this.updateAnswerFieldLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnswerFieldLength() {
|
||||||
|
this.answer_tf.style.width = this.answer_tf.value.length + "ch";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('singlechoice-task', SingleChoiceTask);
|
||||||
|
customElements.define('openended-task', OpenEndedTask);
|
||||||
|
customElements.define('numberconversion-task', NumberConversionTask);
|
||||||
|
|
||||||
17
js/terminal.js
Normal file
17
js/terminal.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
function submit_command() {
|
||||||
|
let terminal_input = document.getElementById('terminal_input');
|
||||||
|
let terminal_output = document.getElementById('terminal_output');
|
||||||
|
|
||||||
|
let cmd = terminal_input.value.trim();
|
||||||
|
terminal_input.disabled = true;
|
||||||
|
if (cmd !== "") {
|
||||||
|
terminal_output.value += ">> " + cmd + "\n";
|
||||||
|
let req = {"action": "execute_cli_command", "cmd" : cmd};
|
||||||
|
request(req).then((resp) => {
|
||||||
|
terminal_output.value += resp + "\n\n";
|
||||||
|
terminal_output.scrollTo(0, terminal_output.scrollHeight);
|
||||||
|
terminal_input.value = "";
|
||||||
|
terminal_input.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
199
js/testground.js
199
js/testground.js
@ -20,9 +20,9 @@ function populate_infobox(test_data, view_only) {
|
|||||||
if (test_concluded) {
|
if (test_concluded) {
|
||||||
let summary = test_data["summary"];
|
let summary = test_data["summary"];
|
||||||
let correct_answer_n = summary["correct_answer_n"];
|
let correct_answer_n = summary["correct_answer_n"];
|
||||||
let challenge_n = summary["challenge_n"];
|
let task_n = summary["challenge_n"];
|
||||||
let r = Math.ceil((correct_answer_n / challenge_n) * 100);
|
let r = Math.ceil((correct_answer_n / task_n) * 100);
|
||||||
percentageS.innerHTML = `${r}% (${correct_answer_n}/${challenge_n})`;
|
percentageS.innerHTML = `${r}% (${correct_answer_n}/${task_n})`;
|
||||||
|
|
||||||
let start_time = unix_time_to_human_readable(test_data["start_time"]);
|
let start_time = unix_time_to_human_readable(test_data["start_time"]);
|
||||||
let end_time = unix_time_to_human_readable(test_data["end_time"]);
|
let end_time = unix_time_to_human_readable(test_data["end_time"]);
|
||||||
@ -70,87 +70,22 @@ function populate_infobox(test_data, view_only) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assemble_answer_radio_id(challenge_N, answer_N) {
|
function populate_tasks(tasks, concluded, view_only = false, gameid) {
|
||||||
return challenge_N + "_" + answer_N;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mark_answers(challenges, view_only = false) {
|
|
||||||
for (let i = 0; i < challenges.length; i++) {
|
|
||||||
let marked_answerR = document.getElementById(assemble_answer_radio_id(i, challenges[i]["player_answer"]));
|
|
||||||
if (marked_answerR !== null) {
|
|
||||||
marked_answerR.checked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function populate_challenges(challenges, concluded, view_only = false, gameid) {
|
|
||||||
let test_display = document.getElementById("test_display");
|
let test_display = document.getElementById("test_display");
|
||||||
test_display.innerHTML = "";
|
test_display.innerHTML = "";
|
||||||
|
|
||||||
let challenge_N = 0;
|
tasks.forEach((task) => {
|
||||||
challenges.forEach((challenge) => {
|
let task_element = document.createElement(`${task["type"]}-task`);
|
||||||
let challenge_N_snapshot = challenge_N;
|
task_element.uploadAnswerCb = save_answer;
|
||||||
let challenge_box = document.createElement("section");
|
|
||||||
challenge_box.classList.add("challenge");
|
|
||||||
let question = document.createElement("span");
|
|
||||||
question.classList.add("question");
|
|
||||||
question.innerHTML = preprocess_inserts(challenge["question"]);
|
|
||||||
let answer_container = document.createElement("section");
|
|
||||||
answer_container.classList.add("answer-container");
|
|
||||||
challenge_box.append(question, answer_container);
|
|
||||||
|
|
||||||
if (challenge["image_url"] !== "") {
|
if (task["image_url"] !== "") {
|
||||||
let qimg = document.createElement("img");
|
task_element.imgUrl = `interface.php?action=get_image&gameid=${gameid}&img_url=${task["image_url"]}`
|
||||||
qimg.src = `interface.php?action=get_image&gameid=${gameid}&img_url=${challenge["image_url"]}`;
|
|
||||||
qimg.classList.add("question-image")
|
|
||||||
challenge_box.insertBefore(qimg, answer_container);
|
|
||||||
}
|
}
|
||||||
|
task_element.isConcluded = concluded;
|
||||||
let seq_num_section = document.createElement("section");
|
task_element.isViewOnly = view_only;
|
||||||
seq_num_section.innerText = String(challenge_N + 1) + "."
|
task_element.fromArray(task);
|
||||||
seq_num_section.classList.add("seq-num");
|
test_display.appendChild(task_element);
|
||||||
challenge_box.append(seq_num_section);
|
|
||||||
|
|
||||||
let answer_N = 0;
|
|
||||||
let player_answer = challenge["player_answer"];
|
|
||||||
player_answer = (player_answer !== "") ? Number(player_answer) : -1;
|
|
||||||
challenge["answers"].forEach((answer) => {
|
|
||||||
let answer_section = document.createElement("section");
|
|
||||||
answer_section.classList.add("answer");
|
|
||||||
let answer_radio = document.createElement("input");
|
|
||||||
answer_radio.type = "radio";
|
|
||||||
answer_radio.id = `${challenge_N}_${answer_N}`;
|
|
||||||
answer_radio.name = `challenge_${challenge_N}`;
|
|
||||||
answer_radio.disabled = concluded || view_only;
|
|
||||||
let answer_N_snapshot = answer_N;
|
|
||||||
answer_radio.addEventListener("input", () => {
|
|
||||||
save_answer(challenge_N_snapshot, answer_N_snapshot);
|
|
||||||
});
|
|
||||||
|
|
||||||
let answer_text = document.createElement("label");
|
|
||||||
answer_text.innerHTML = preprocess_inserts(answer);
|
|
||||||
answer_text.setAttribute("for", answer_radio.id);
|
|
||||||
if (concluded && (challenge["correct_answer"] === answer_N)) {
|
|
||||||
answer_text.classList.add("correct-answer")
|
|
||||||
|
|
||||||
if (player_answer !== challenge["correct_answer"]) {
|
|
||||||
challenge_box.classList.add("bad-answer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
answer_section.append(answer_radio, answer_text);
|
|
||||||
answer_container.appendChild(answer_section);
|
|
||||||
|
|
||||||
answer_N++;
|
|
||||||
});
|
|
||||||
challenge_N++;
|
|
||||||
|
|
||||||
test_display.appendChild(challenge_box);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mark_answers(challenges, view_only);
|
|
||||||
|
|
||||||
MathJax.typeset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function populate_all(test_id, gameid, view_only) {
|
function populate_all(test_id, gameid, view_only) {
|
||||||
@ -162,22 +97,33 @@ function populate_all(test_id, gameid, view_only) {
|
|||||||
request(req).then(resp => {
|
request(req).then(resp => {
|
||||||
TEST_DATA = JSON.parse(resp);
|
TEST_DATA = JSON.parse(resp);
|
||||||
let concluded = TEST_DATA["state"] === "concluded";
|
let concluded = TEST_DATA["state"] === "concluded";
|
||||||
populate_challenges(TEST_DATA["challenges"], concluded, view_only, gameid);
|
populate_tasks(TEST_DATA["challenges"], concluded, view_only, gameid);
|
||||||
populate_infobox(TEST_DATA, view_only);
|
populate_infobox(TEST_DATA, view_only);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_answer(chidx, aidx) {
|
function save_answer(tidx, ans) {
|
||||||
let req = {
|
let req = {
|
||||||
action: "save_answer",
|
action: "save_answer",
|
||||||
testid: TEST_DATA["_id"],
|
testid: TEST_DATA["_id"],
|
||||||
challenge_index: chidx,
|
task_index: tidx,
|
||||||
answer_index: aidx,
|
answer: ans,
|
||||||
};
|
};
|
||||||
request(req);
|
request(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function save_all_answers() {
|
||||||
|
let tasks = document.getElementById("test_display").children;
|
||||||
|
for (let i = 0; i < tasks.length; i++) {
|
||||||
|
tasks[i].uploadAnswer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function submit_test() {
|
function submit_test() {
|
||||||
|
// first, save all answers
|
||||||
|
save_all_answers();
|
||||||
|
|
||||||
|
// then signal test submission
|
||||||
let req = {
|
let req = {
|
||||||
action: "submit_test",
|
action: "submit_test",
|
||||||
testid: TEST_DATA["_id"]
|
testid: TEST_DATA["_id"]
|
||||||
@ -185,4 +131,91 @@ function submit_test() {
|
|||||||
request(req).then(resp => {
|
request(req).then(resp => {
|
||||||
populate_all(TEST_DATA["_id"], TEST_DATA["gameid"], false);
|
populate_all(TEST_DATA["_id"], TEST_DATA["gameid"], false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------
|
||||||
|
|
||||||
|
// function populate_tasks(tasks, concluded, view_only = false, gameid) {
|
||||||
|
// let test_display = document.getElementById("test_display");
|
||||||
|
// test_display.innerHTML = "";
|
||||||
|
//
|
||||||
|
// let task_N = 0;
|
||||||
|
// tasks.forEach((task) => {
|
||||||
|
// let task_N_snapshot = task_N;
|
||||||
|
// let task_box = document.createElement("section");
|
||||||
|
// task_box.classList.add("task");
|
||||||
|
// let question = document.createElement("span");
|
||||||
|
// question.classList.add("question");
|
||||||
|
// question.innerHTML = preprocess_inserts(task["question"]);
|
||||||
|
// let answer_container = document.createElement("section");
|
||||||
|
// answer_container.classList.add("answer-container");
|
||||||
|
// task_box.append(question, answer_container);
|
||||||
|
//
|
||||||
|
// if (task["image_url"] !== "") {
|
||||||
|
// let qimg = document.createElement("img");
|
||||||
|
// qimg.src = `interface.php?action=get_image&gameid=${gameid}&img_url=${task["image_url"]}`;
|
||||||
|
// qimg.classList.add("question-image")
|
||||||
|
// task_box.insertBefore(qimg, answer_container);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let seq_num_section = document.createElement("section");
|
||||||
|
// seq_num_section.innerText = String(task_N + 1) + "."
|
||||||
|
// seq_num_section.classList.add("seq-num");
|
||||||
|
// task_box.append(seq_num_section);
|
||||||
|
//
|
||||||
|
// let answer_N = 0;
|
||||||
|
// let player_answer = task["player_answer"];
|
||||||
|
// player_answer = (player_answer !== "") ? Number(player_answer) : -1;
|
||||||
|
// task["answers"].forEach((answer) => {
|
||||||
|
// let answer_section = document.createElement("section");
|
||||||
|
// answer_section.classList.add("answer");
|
||||||
|
// let answer_radio = document.createElement("input");
|
||||||
|
// answer_radio.type = "radio";
|
||||||
|
// answer_radio.id = `${task_N}_${answer_N}`;
|
||||||
|
// answer_radio.name = `task_${task_N}`;
|
||||||
|
// answer_radio.disabled = concluded || view_only;
|
||||||
|
// let answer_N_snapshot = answer_N;
|
||||||
|
// answer_radio.addEventListener("input", () => {
|
||||||
|
// save_answer(task_N_snapshot, answer_N_snapshot);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// let answer_text = document.createElement("label");
|
||||||
|
// answer_text.innerHTML = preprocess_inserts(answer);
|
||||||
|
// answer_text.setAttribute("for", answer_radio.id);
|
||||||
|
// if (concluded && (task["correct_answer"] === answer_N)) {
|
||||||
|
// answer_text.classList.add("correct-answer")
|
||||||
|
//
|
||||||
|
// if (player_answer !== task["correct_answer"]) {
|
||||||
|
// task_box.classList.add("bad-answer");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// answer_section.append(answer_radio, answer_text);
|
||||||
|
// answer_container.appendChild(answer_section);
|
||||||
|
//
|
||||||
|
// answer_N++;
|
||||||
|
// });
|
||||||
|
// task_N++;
|
||||||
|
//
|
||||||
|
// test_display.appendChild(task_box);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// mark_answers(tasks, view_only);
|
||||||
|
//
|
||||||
|
// MathJax.typeset();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// function assemble_answer_radio_id(task_N, answer_N) {
|
||||||
|
// return task_N + "_" + answer_N;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// function mark_answers(tasks, view_only = false) {
|
||||||
|
// for (let i = 0; i < tasks.length; i++) {
|
||||||
|
// let marked_answerR = document.getElementById(assemble_answer_radio_id(i, tasks[i]["player_answer"]));
|
||||||
|
// if (marked_answerR !== null) {
|
||||||
|
// marked_answerR.checked = true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@ -83,7 +83,7 @@ function create_edit_user(user = null) {
|
|||||||
nicknameF.value = user["nickname"];
|
nicknameF.value = user["nickname"];
|
||||||
nicknameF.readOnly = true;
|
nicknameF.readOnly = true;
|
||||||
realnameF.value = user["realname"];
|
realnameF.value = user["realname"];
|
||||||
passwordF.type = "password";
|
passwordF.task_type = "password";
|
||||||
passwordF.value = "";
|
passwordF.value = "";
|
||||||
passwordF.readOnly = false;
|
passwordF.readOnly = false;
|
||||||
groupsF.value = "";
|
groupsF.value = "";
|
||||||
@ -97,7 +97,7 @@ function create_edit_user(user = null) {
|
|||||||
nicknameF.value = "";
|
nicknameF.value = "";
|
||||||
nicknameF.readOnly = false;
|
nicknameF.readOnly = false;
|
||||||
realnameF.value = "";
|
realnameF.value = "";
|
||||||
passwordF.type = "text";
|
passwordF.task_type = "text";
|
||||||
passwordF.value = generateRandomString();
|
passwordF.value = generateRandomString();
|
||||||
passwordF.readOnly = true;
|
passwordF.readOnly = true;
|
||||||
groupsF.value = "";
|
groupsF.value = "";
|
||||||
|
|||||||
34
main.php
34
main.php
@ -29,8 +29,15 @@ $privilege = $user_data["privilege"];
|
|||||||
<script src="js/req.js"></script>
|
<script src="js/req.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<script src="js/spreadquiz.js"></script>
|
<script src="js/spreadquiz.js"></script>
|
||||||
|
<?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
|
||||||
|
<script src="js/quizmaster_common.js"></script>
|
||||||
|
<script src="js/terminal.js"></script>
|
||||||
|
<?php } ?>
|
||||||
<link rel="stylesheet" href="style/spreadquiz.css"/>
|
<link rel="stylesheet" href="style/spreadquiz.css"/>
|
||||||
<link rel="stylesheet" href="style/spreadquiz_mobile.css"/>
|
<link rel="stylesheet" href="style/spreadquiz_mobile.css"/>
|
||||||
|
<?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
|
||||||
|
<link rel="stylesheet" href="style/quizmaster_area.css"/>
|
||||||
|
<?php } ?>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="screen_panel">
|
<section id="screen_panel">
|
||||||
@ -52,16 +59,37 @@ $privilege = $user_data["privilege"];
|
|||||||
<section id="action_panel" class="info-pane-element">
|
<section id="action_panel" class="info-pane-element">
|
||||||
<?php if (($privilege === PRIVILEGE_CREATOR) || ($privilege === PRIVILEGE_QUIZMASTER)) { ?>
|
<?php if (($privilege === PRIVILEGE_CREATOR) || ($privilege === PRIVILEGE_QUIZMASTER)) { ?>
|
||||||
<input type="button" value="Nyitólap" onclick="open_in_content_frame('default_frame.php')">
|
<input type="button" value="Nyitólap" onclick="open_in_content_frame('default_frame.php')">
|
||||||
<input type="button" value="Tartalmak kezelése" onclick="open_in_content_frame('game_manager_frame.php')">
|
<input type="button" value="Tartalmak kezelése"
|
||||||
|
onclick="open_in_content_frame('game_manager_frame.php')">
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
<?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
|
<?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
|
||||||
<input type="button" value="Felhasználók kezelése" onclick="open_in_content_frame('user_manager_frame.php')">
|
<input type="button" value="Felhasználók kezelése"
|
||||||
<input type="button" value="Csoportok kezelése" onclick="open_in_content_frame('group_manager_frame.php')">
|
onclick="open_in_content_frame('user_manager_frame.php')">
|
||||||
|
<input type="button" value="Csoportok kezelése"
|
||||||
|
onclick="open_in_content_frame('group_manager_frame.php')">
|
||||||
|
<input type="button" value="Terminál" onclick="show('terminal_window')">
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</section>
|
</section>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
<?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
|
||||||
|
<section class="window" shown="false" id="terminal_window">
|
||||||
|
<section class="window-inner">
|
||||||
|
<textarea class="terminal-style" style="height: 20em;" readonly placeholder="(kimenet)" id="terminal_output"></textarea><br>
|
||||||
|
<input type="text" class="terminal-style" id="terminal_input" placeholder="(parancs)"><br>
|
||||||
|
<input type="button" onclick="hide('terminal_window')" value="Bezárás">
|
||||||
|
<script>
|
||||||
|
let term_input = document.getElementById("terminal_input");
|
||||||
|
term_input.addEventListener("keypress", function (e) {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
submit_command();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<?php } ?>
|
||||||
<section class="window" shown="false" id="change_password_window">
|
<section class="window" shown="false" id="change_password_window">
|
||||||
<section class="window-inner">
|
<section class="window-inner">
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
<script src="js/common.js"></script>
|
<script src="js/common.js"></script>
|
||||||
<link rel="stylesheet" href="style/spreadquiz.css">
|
<link rel="stylesheet" href="style/spreadquiz.css">
|
||||||
<link rel="stylesheet" href="style/quizmaster_area.css"/>
|
<link rel="stylesheet" href="style/quizmaster_area.css"/>
|
||||||
|
<link rel="stylesheet" href="style/report.css"/>
|
||||||
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -132,4 +132,10 @@ span.answer[correct=true] {
|
|||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
background-color: #176767;
|
background-color: #176767;
|
||||||
border-radius: 0.3em;
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-style {
|
||||||
|
font-family: 'Monaco', monospace;
|
||||||
|
width: 40em;
|
||||||
|
font-size: 12pt;
|
||||||
}
|
}
|
||||||
74
style/report.css
Normal file
74
style/report.css
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/* FIXME: ezek mind át lettek téve a task custom-elementbe */
|
||||||
|
|
||||||
|
section.task {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 40em;
|
||||||
|
/*border: 1px solid black;*/
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #d3e5e5;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.question {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #176767;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.answer-container {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
section.answer {
|
||||||
|
margin: 0.3em 0.8em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.answer label {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
padding: 0.3em 0.5em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 85%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.answer label.correct-answer {
|
||||||
|
border: 2px solid #176767 !important;
|
||||||
|
background-color: #176767;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
|
||||||
|
background-color: #176767;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: 'Monaco', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.question-image {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
margin: 1em auto;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.seq-num {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.5em;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #176767;
|
||||||
|
color: whitesmoke;
|
||||||
|
border-bottom-right-radius: 0.3em;
|
||||||
|
border-top-left-radius: 0.3em;
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
@ -229,55 +229,6 @@ section#test_area {
|
|||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.challenge {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 40em;
|
|
||||||
/*border: 1px solid black;*/
|
|
||||||
padding: 1em;
|
|
||||||
background-color: #d3e5e5;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.question {
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #176767;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.answer-container {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
section.answer {
|
|
||||||
margin: 0.3em 0.8em;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.answer label {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
padding: 0.3em 0.5em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 85%;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.answer label.correct-answer {
|
|
||||||
border: 2px solid #176767 !important;
|
|
||||||
background-color: #176767;
|
|
||||||
color: whitesmoke;
|
|
||||||
/*padding: 0.1em;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
|
|
||||||
background-color: #176767;
|
|
||||||
color: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#infobox {
|
section#infobox {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -368,39 +319,6 @@ section.test-summary-record {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: 'Monaco', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.question-image {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
margin: 1em auto;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section.seq-num {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
padding: 0.5em;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #176767;
|
|
||||||
color: whitesmoke;
|
|
||||||
border-bottom-right-radius: 0.3em;
|
|
||||||
border-top-left-radius: 0.3em;
|
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
section#further-info {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
padding: 0.4em 0;
|
padding: 0.4em 0;
|
||||||
|
|||||||
@ -34,13 +34,15 @@
|
|||||||
height: 12em;
|
height: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.challenge {
|
/*!* FIXME: áttéve *!*/
|
||||||
width: calc(100vw - 3em);
|
/*section.task {*/
|
||||||
}
|
/* width: calc(100vw - 3em);*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
section.answer label {
|
/*!* FIXME: áttéve *!*/
|
||||||
max-width: 80%;
|
/*section.answer label {*/
|
||||||
}
|
/* max-width: 80%;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
section#infobox {
|
section#infobox {
|
||||||
top: unset;
|
top: unset;
|
||||||
|
|||||||
@ -30,9 +30,12 @@ if ($testid === "") {
|
|||||||
<script src="js/o.js"></script>
|
<script src="js/o.js"></script>
|
||||||
<script src="js/common.js"></script>
|
<script src="js/common.js"></script>
|
||||||
<script src="js/testground.js"></script>
|
<script src="js/testground.js"></script>
|
||||||
|
<script src="js/tasks.js"></script>
|
||||||
<link rel="stylesheet" href="style/spreadquiz.css">
|
<link rel="stylesheet" href="style/spreadquiz.css">
|
||||||
<link rel="stylesheet" href="style/spreadquiz_mobile.css">
|
<link rel="stylesheet" href="style/spreadquiz_mobile.css">
|
||||||
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||||
|
<script type="text/javascript" src="https://nturley.github.io/netlistsvg/elk.bundled.js"></script>
|
||||||
|
<script type="text/javascript" src="https://nturley.github.io/netlistsvg/built/netlistsvg.bundle.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="test_display">
|
<section id="test_display">
|
||||||
@ -66,6 +69,11 @@ if ($testid === "") {
|
|||||||
</section>
|
</section>
|
||||||
<script>
|
<script>
|
||||||
populate_all("<?=$testid ?>", "<?=$gameid ?>", <?=$view_only ?>);
|
populate_all("<?=$testid ?>", "<?=$gameid ?>", <?=$view_only ?>);
|
||||||
|
window.onbeforeunload = () => {
|
||||||
|
if (TEST_DATA["state"] !== "concluded") {
|
||||||
|
save_all_answers();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user