ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
Repository.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
26use ILIAS\Refinery\Factory as Refinery;
28
30{
32
33 public function __construct(
34 private readonly \ilDBInterface $db,
35 private readonly Refinery $refinery,
36 private readonly MarksRepository $marks_repository,
37 Services $global_cache
38 ) {
39 $this->cache = $global_cache->get(new BaseRequest('test_result'));
40 }
41
45 public function getPassedParticipants(int $test_obj_id): array
46 {
47 $result = $this->db->queryF(
48 'SELECT tst_result_cache.active_fi AS active_id, tst_active.user_fi AS user_id FROM tst_result_cache' . PHP_EOL
49 . 'INNER JOIN tst_active ON tst_active.active_id = tst_result_cache.active_fi' . PHP_EOL
50 . 'INNER JOIN tst_tests ON tst_tests.test_id = tst_active.test_fi' . PHP_EOL
51 . 'WHERE tst_tests.obj_fi = %s AND tst_result_cache.passed_once = 1' . PHP_EOL,
53 [$test_obj_id]
54 );
55 return $this->db->fetchAll($result);
56 }
57
58 public function isPassed(int $user_id, int $test_obj_id): bool
59 {
60 return ($status = $this->readOrQueryStatus($user_id, $test_obj_id)) !== null && $status['passed'];
61 }
62
63 public function isFailed(int $user_id, int $test_obj_id): bool
64 {
65 return ($status = $this->readOrQueryStatus($user_id, $test_obj_id)) !== null && $status['failed'];
66 }
67
68 public function hasFinished(int $user_id, int $test_obj_id): bool
69 {
70 return ($status = $this->readOrQueryStatus($user_id, $test_obj_id)) !== null && $status['finished'];
71 }
72
73 public function getTestResult(int $active_id): ?ParticipantResult
74 {
75 $result = $this->db->queryF(
76 'SELECT tst_result_cache.*, tst_active.test_fi AS test_id FROM tst_result_cache' . PHP_EOL
77 . 'JOIN tst_active ON tst_result_cache.active_fi = tst_active.active_id' . PHP_EOL
78 . 'WHERE active_fi = %s',
80 [$active_id]
81 );
82
83 return $this->toParticipantResult($this->db->fetchAssoc($result));
84 }
85
86 public function updateTestResultCache(int $active_id, ?\ilAssQuestionProcessLocker $process_locker = null): ?ParticipantResult
87 {
88 $attempt = \ilObjTest::_getResultPass($active_id);
89 $attempt_result = $this->fetchTestAttemptResult($active_id, $attempt);
90 if (!$attempt_result) {
91 return null;
92 }
93
94 // Prevent unfinished passes from being entered in the table so that no inconsistencies occur during an attempt
95 $status = StatusOfAttempt::build(
96 $attempt,
97 $attempt_result['last_finished_pass'],
98 $attempt_result['finalized_by'],
99 );
100
101 $result = $this->buildTestResultObject($attempt_result);
102 $callback = function () use ($result) {
103 $values = [
104 'active_fi' => [\ilDBConstants::T_INTEGER, $result->getActiveId()],
105 'pass' => [\ilDBConstants::T_INTEGER, $result->getAttempt()],
106 'max_points' => [\ilDBConstants::T_FLOAT, $result->getMaxPoints()],
107 'reached_points' => [\ilDBConstants::T_FLOAT, $result->getReachedPoints()],
108 'mark_short' => [\ilDBConstants::T_TEXT, $result->getMarkShort()],
109 'mark_official' => [\ilDBConstants::T_TEXT, $result->getMarkOfficial()],
110 'passed_once' => [\ilDBConstants::T_INTEGER, $result->isPassedOnce()],
111 'passed' => [\ilDBConstants::T_INTEGER, (int) $result->isPassed()],
112 'failed' => [\ilDBConstants::T_INTEGER, (int) $result->isFailed()],
113 'tstamp' => [\ilDBConstants::T_INTEGER, time()],
114 ];
115 $this->db->replace(
116 'tst_result_cache',
117 ['active_fi' => $result->getActiveId()],
118 $values
119 );
120 };
121
122 if (is_object($process_locker)) {
123 $process_locker->executeUserTestResultUpdateLockOperation($callback);
124 } else {
125 $callback();
126 }
127
128 $this->updateStatusCache(
129 $attempt_result['user_id'],
130 $attempt_result['test_obj_id'],
131 [
132 'passed' => $result->isPassed(),
133 'failed' => $result->isFailed(),
134 'finished' => $status->isFinished(),
135 ]
136 );
137
138 return $result;
139 }
140
141 public function getTestAttemptResult(int $active_id): ?AttemptResult
142 {
143 $result = $this->db->queryF(
144 "SELECT * FROM tst_pass_result WHERE active_fi = %s",
146 [$active_id]
147 );
148 return $this->toTestAttemptResult($this->db->fetchAssoc($result));
149 }
150
151 public function updateTestAttemptResult(
152 int $active_id,
153 int $attempt,
154 ?\ilAssQuestionProcessLocker $process_locker = null,
155 ?int $test_obj_id = null,
156 bool $update_result_cache_table = true
157 ): ?AttemptResult {
158 $test_result = $this->fetchTestResult($active_id, $attempt);
159 if (!$test_result) {
160 return null;
161 }
162
163 $result_object = $this->buildTestAttemptResultObject(
164 $active_id,
165 $test_result,
166 $test_obj_id
167 );
168
169 $callback = function () use ($result_object, $attempt) {
170 $this->db->replace(
171 'tst_pass_result',
172 [
173 'active_fi' => [\ilDBConstants::T_INTEGER, $result_object->getActiveId()],
174 'pass' => [\ilDBConstants::T_INTEGER, $attempt]
175 ],
176 [
177 'points' => [\ilDBConstants::T_FLOAT, $result_object->getReachedPoints()],
178 'maxpoints' => [\ilDBConstants::T_FLOAT, $result_object->getMaxPoints()],
179 'questioncount' => [\ilDBConstants::T_INTEGER, $result_object->getQuestionCount()],
180 'answeredquestions' => [\ilDBConstants::T_INTEGER, $result_object->getAnsweredQuestions()],
181 'workingtime' => [\ilDBConstants::T_INTEGER, $result_object->getWorkingTime()],
182 'tstamp' => [\ilDBConstants::T_INTEGER, time()],
183 'exam_id' => [\ilDBConstants::T_TEXT, $result_object->getExamId()],
184 'finalized_by' => [\ilDBConstants::T_TEXT, $result_object->getFinalizedBy()]
185 ]
186 );
187 };
188
189 if (is_object($process_locker)) {
190 $process_locker->executeUserPassResultUpdateLockOperation($callback);
191 } else {
192 $callback();
193 }
194
195 if ($update_result_cache_table) {
196 $this->updateTestResultCache($active_id, $process_locker);
197 }
198
199 return $result_object;
200 }
201
202 public function finalizeTestAttemptResult(int $active_id, int $attempt, StatusOfAttempt $status_of_attempt): void
203 {
204 if (!$status_of_attempt->isFinished()) {
205 throw new \RuntimeException('Status of attempt must be finished to finalize test attempt result');
206 }
207
208 $this->db->manipulateF(
209 'UPDATE tst_pass_result SET tstamp = %s, finalized_by = %s WHERE active_fi = %s AND pass = %s',
210 ['integer', 'text', 'integer', 'integer'],
211 [time(), $status_of_attempt->value, $active_id, $attempt]
212 );
213 }
214
215 private function fetchTestAttemptResult(int $active_id, int $attempt): ?array
216 {
217 return $this->db->fetchAssoc($this->db->queryF(
218 "SELECT tst_pass_result.*, tst_active.last_finished_pass, tst_active.user_fi AS user_id, tst_tests.test_id,
219 tst_tests.obj_fi AS test_obj_id, tst_pass_result.maxpoints AS max_points, points AS reached_points,
220 tst_result_cache.passed_once AS passed_once_before
221 FROM tst_pass_result
222 INNER JOIN tst_active ON tst_pass_result.active_fi = tst_active.active_id
223 INNER JOIN tst_tests ON tst_tests.test_id = tst_active.test_fi
224 LEFT JOIN tst_result_cache ON tst_result_cache.active_fi = tst_active.active_id
225 WHERE tst_pass_result.active_fi = %s AND tst_pass_result.pass = %s",
227 [$active_id, $attempt]
228 ));
229 }
230
231 private function buildTestResultObject(array $test_attempt_result_array): ParticipantResult
232 {
233 $test_attempt_result = $this->toParticipantResult($test_attempt_result_array);
234
235 $is_passed = $test_attempt_result->getAttempt() <= $test_attempt_result_array['last_finished_pass'] && $test_attempt_result->isPassed();
236 $passed_once_before = (bool) ($test_attempt_result_array['passed_once_before'] ?? false);
237 return $test_attempt_result->withPassedOnce($is_passed || $passed_once_before);
238 }
239
240 private function fetchTestResult(int $active_id, int $attempt): ?array
241 {
242 return $this->db->fetchAssoc($this->db->queryF(
243 'SELECT r.pass,' . PHP_EOL
244 . 'SUM(r.points) AS points,' . PHP_EOL
245 . 'COUNT(DISTINCT(r.question_fi)) answeredquestions,' . PHP_EOL
246 . 'pr.exam_id,' . PHP_EOL
247 . 'pr.finalized_by' . PHP_EOL
248 . 'FROM tst_test_result r' . PHP_EOL
249 . 'INNER JOIN tst_pass_result pr' . PHP_EOL
250 . 'ON r.active_fi = pr.active_fi AND r.pass = pr.pass' . PHP_EOL
251 . 'WHERE r.active_fi = %s AND r.pass = %s',
253 [$active_id, $attempt]
254 ));
255 }
256
257 private function buildTestAttemptResultObject(int $active_id, array $test_result, ?int $test_obj_id): AttemptResult
258 {
259 $test_result['active_fi'] = $active_id;
260 $test_attempt_result = $this->toTestAttemptResult($test_result);
261 $additional_data = $this->fetchAdditionalTestData($test_attempt_result->getActiveId(), $test_attempt_result->getAttempt());
262
263 return $test_attempt_result->withMaxPoints($additional_data['max_points'])
264 ->withQuestionCount($additional_data['question_count'])
265 ->withWorkingTime(
266 $this->fetchWorkingTime($test_attempt_result->getActiveId(), $test_attempt_result->getAttempt())
267 )
268 ->withExamId(
270 $test_attempt_result->getActiveId(),
271 $test_attempt_result->getAttempt(),
272 $test_obj_id
273 )
274 )
275 ->withTimestamp();
276 }
277
281 private function fetchAdditionalTestData(int $active_id, int $attempt): array
282 {
283 $result = $this->db->queryF(
284 "SELECT tst_tests.question_set_type FROM tst_active
285 INNER JOIN tst_tests ON tst_active.test_fi = tst_tests.test_id
286 WHERE tst_active.active_id = %s",
288 [$active_id]
289 );
290 $question_set_type = $result->numRows() > 0 ? $this->db->fetchAssoc($result)['question_set_type'] : '';
291
292 $result = match ($question_set_type) {
293 \ilObjTest::QUESTION_SET_TYPE_RANDOM => $this->db->queryF(
294 "SELECT tst_test_rnd_qst.pass, COUNT(tst_test_rnd_qst.question_fi) qcount, SUM(qpl_questions.points) qsum
295 FROM tst_test_rnd_qst, qpl_questions
296 WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
297 AND tst_test_rnd_qst.active_fi = %s AND pass = %s
298 GROUP BY tst_test_rnd_qst.active_fi, tst_test_rnd_qst.pass",
300 [$active_id, $attempt]
301 ),
302 \ilObjTest::QUESTION_SET_TYPE_FIXED => $this->db->queryF(
303 "SELECT COUNT(tst_test_question.question_fi) qcount, SUM(qpl_questions.points) qsum
304 FROM tst_test_question, qpl_questions, tst_active
305 WHERE tst_test_question.question_fi = qpl_questions.question_id
306 AND tst_test_question.test_fi = tst_active.test_fi AND tst_active.active_id = %s
307 GROUP BY tst_test_question.test_fi",
309 [$active_id]
310 ),
311 default => throw new \ilTestException('not supported question set type: ' . $question_set_type),
312 };
313
314 $row = $this->db->fetchAssoc($result);
315 return is_array($row)
316 ? ['question_count' => (int) $row['qcount'], 'max_points' => (float) $row['qsum']]
317 : ['question_count' => 0, 'max_points' => 0.0];
318 }
319
320 public function fetchWorkingTime(int $active_id, int $attempt): int
321 {
322 $result = $this->db->queryF(
323 "SELECT started, finished FROM tst_times WHERE active_fi = %s AND pass = %s ORDER BY started",
325 [$active_id, $attempt]
326 );
327
328 $time = 0;
329 while ($row = $this->db->fetchAssoc($result)) {
330 $time += (strtotime($row['finished']) - strtotime($row['started']));
331 }
332 return $time;
333 }
334
335 public function removeTestResults(array $active_ids, int $test_obj_id): void
336 {
337 $condition = $this->db->in('active_fi', $active_ids, false, \ilDBConstants::T_INTEGER);
338
339 $this->db->manipulate("DELETE FROM tst_test_result WHERE {$condition}");
340 $this->db->manipulate("DELETE FROM tst_pass_result WHERE {$condition}");
341
342 $user_ids = $this->db->fetchAll(
343 $this->db->query(
344 'SELECT user_fi FROM tst_active WHERE' . PHP_EOL
345 . $this->db->in('active_id', $active_ids, false, \ilDBConstants::T_INTEGER)
346 )
347 );
348 foreach ($user_ids as $row) {
349 $this->cache->delete($row['user_fi'] . ':' . $test_obj_id);
350 }
351 }
352
353
354 private function toParticipantResult(?array $row): ?ParticipantResult
355 {
356 if ($row === null) {
357 return null;
358 }
359
360 $max_points = $this->ensurePositive($row['max_points'] ?? 0.0);
361 $reached_points = $this->ensurePositive($row['reached_points'] ?? 0.0);
362 $percentage = ($max_points > 0 ? $reached_points / $max_points : 0.0) * 100;
363
364 $mark = $this->marks_repository->getMarkSchemaFor($row['test_id'])->getMatchingMark($percentage);
365
366 return new ParticipantResult(
367 $row['active_fi'],
368 (int) $row['pass'],
369 $max_points,
370 $reached_points,
371 $mark,
372 (int) ($row['tstamp'] ?? -1),
373 (bool) ($row['passed_once'] ?? false),
374 );
375 }
376
377 private function toTestAttemptResult(?array $row): ?AttemptResult
378 {
379 if ($row === null) {
380 return null;
381 }
382
383 return new AttemptResult(
384 $row['active_fi'],
385 (int) $row['pass'],
386 $this->ensurePositive($row['maxpoints'] ?? 0.0),
387 $this->ensurePositive($row['points'] ?? 0.0),
388 (int) ($row['questioncount'] ?? 0),
389 (int) ($row['answeredquestions'] ?? 0),
390 (int) ($row['workingtime'] ?? 0),
391 (int) ($row['tstamp'] ?? -1),
392 $row['exam_id'] ?? '',
393 $row['finalized_by'] ?? '',
394 );
395 }
396
397 private function ensurePositive(mixed $value): float
398 {
399 return max(0.0, (float) $value);
400 }
401
405 private function updateStatusCache(int $user_id, int $test_obj_id, array $status): void
406 {
407 $this->cache->set($user_id . ':' . $test_obj_id, $status);
408 }
409
413 private function readOrQueryStatus(int $user_id, int $test_obj_id): ?array
414 {
415 $cached_status = $this->cache->get($user_id . ':' . $test_obj_id, $this->refinery->identity());
416 if ($cached_status !== null) {
417 return $cached_status;
418 }
419
420 $status = $this->db->fetchAssoc($this->db->queryF(
421 "SELECT tst_result_cache.passed, tst_result_cache.failed, (tst_active.last_finished_pass IS NOT NULL) AS finished
422 FROM tst_result_cache
423 INNER JOIN tst_active ON tst_active.active_id = tst_result_cache.active_fi
424 INNER JOIN tst_tests ON tst_tests.test_id = tst_active.test_fi
425 WHERE tst_active.user_fi = %s AND tst_tests.obj_fi = %s",
427 [$user_id, $test_obj_id]
428 ));
429 if ($status === null) {
430 return null;
431 }
432
433 $this->updateStatusCache($user_id, $test_obj_id, $status);
434 return $status;
435 }
436}
get(Request $for_container)
Definition: Services.php:51
Builds data types.
Definition: Factory.php:36
Class ParticipantResult is a model representation of an entry in the test_result_cache table.
__construct(private readonly \ilDBInterface $db, private readonly Refinery $refinery, private readonly MarksRepository $marks_repository, Services $global_cache)
Definition: Repository.php:33
fetchTestResult(int $active_id, int $attempt)
Definition: Repository.php:240
readOrQueryStatus(int $user_id, int $test_obj_id)
Definition: Repository.php:413
updateStatusCache(int $user_id, int $test_obj_id, array $status)
Definition: Repository.php:405
finalizeTestAttemptResult(int $active_id, int $attempt, StatusOfAttempt $status_of_attempt)
Definition: Repository.php:202
isPassed(int $user_id, int $test_obj_id)
Definition: Repository.php:58
updateTestAttemptResult(int $active_id, int $attempt, ?\ilAssQuestionProcessLocker $process_locker=null, ?int $test_obj_id=null, bool $update_result_cache_table=true)
Definition: Repository.php:151
getPassedParticipants(int $test_obj_id)
Definition: Repository.php:45
fetchWorkingTime(int $active_id, int $attempt)
Definition: Repository.php:320
updateTestResultCache(int $active_id, ?\ilAssQuestionProcessLocker $process_locker=null)
Definition: Repository.php:86
fetchAdditionalTestData(int $active_id, int $attempt)
Definition: Repository.php:281
isFailed(int $user_id, int $test_obj_id)
Definition: Repository.php:63
hasFinished(int $user_id, int $test_obj_id)
Definition: Repository.php:68
buildTestResultObject(array $test_attempt_result_array)
Definition: Repository.php:231
buildTestAttemptResultObject(int $active_id, array $test_result, ?int $test_obj_id)
Definition: Repository.php:257
removeTestResults(array $active_ids, int $test_obj_id)
Definition: Repository.php:335
fetchTestAttemptResult(int $active_id, int $attempt)
Definition: Repository.php:215
static _getResultPass($active_id)
Retrieves the pass number that should be counted for a given user.
const QUESTION_SET_TYPE_RANDOM
const QUESTION_SET_TYPE_FIXED
static buildExamId($active_id, $pass, $test_obj_id=null)
Interface ilDBInterface.