SpreadQuiz/class/Test.php
2025-10-11 18:11:21 +02:00

257 lines
7.3 KiB
PHP

<?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->setGovernor($this); // set the task governor
$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"], $this);
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 = [], string $mode = "all"): array
{
$tasks = [];
foreach ($this->tasks as &$t) {
$tasks[] = $t->toArray($mode);
}
$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) {
$ch->autoCheck();
$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;
}
public function getGameDir(): string {
return Game::getGameDirById($this->gameId);
}
}