ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
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 reachedPercentage(
74 int $a_usr_id,
75 int $a_trigger_obj_id,
76 float $min_threshold,
77 float $max_threshold
78 ): bool {
79 return ($status = $this->readOrQueryStatus($a_usr_id, $a_trigger_obj_id)) !== null
80 && $status['percentage'] >= $min_threshold
81 && $status['percentage'] <= $max_threshold;
82 }
83
84 public function getTestResult(int $active_id): ?ParticipantResult
85 {
86 $result = $this->db->queryF(
87 'SELECT tst_result_cache.*, tst_active.test_fi AS test_id FROM tst_result_cache' . PHP_EOL
88 . 'JOIN tst_active ON tst_result_cache.active_fi = tst_active.active_id' . PHP_EOL
89 . 'WHERE active_fi = %s',
91 [$active_id]
92 );
93
94 return $this->toParticipantResult($this->db->fetchAssoc($result));
95 }
96
97 public function updateTestResultCache(int $active_id, ?\ilAssQuestionProcessLocker $process_locker = null): ?ParticipantResult
98 {
99 $attempt = $this->lookupAttempt($active_id);
100 $attempt_result = $this->fetchTestAttemptResult($active_id, $attempt);
101 if (!$attempt_result) {
102 return null;
103 }
104
105 // Prevent unfinished passes from being entered in the table so that no inconsistencies occur during an attempt
106 $status = StatusOfAttempt::build(
107 $attempt,
108 $attempt_result['last_finished_pass'],
109 $attempt_result['finalized_by'],
110 );
111
112 $result = $this->buildTestResultObject($attempt_result);
113 $callback = function () use ($result) {
114 $values = [
115 'active_fi' => [\ilDBConstants::T_INTEGER, $result->getActiveId()],
116 'pass' => [\ilDBConstants::T_INTEGER, $result->getAttempt()],
117 'max_points' => [\ilDBConstants::T_FLOAT, $result->getMaxPoints()],
118 'reached_points' => [\ilDBConstants::T_FLOAT, $result->getReachedPoints()],
119 'mark_short' => [\ilDBConstants::T_TEXT, $result->getMarkShort()],
120 'mark_official' => [\ilDBConstants::T_TEXT, $result->getMarkOfficial()],
121 'passed_once' => [\ilDBConstants::T_INTEGER, $result->isPassedOnce()],
122 'passed' => [\ilDBConstants::T_INTEGER, (int) $result->isPassed()],
123 'failed' => [\ilDBConstants::T_INTEGER, (int) $result->isFailed()],
124 'tstamp' => [\ilDBConstants::T_INTEGER, time()],
125 ];
126 $this->db->replace(
127 'tst_result_cache',
128 ['active_fi' => $result->getActiveId()],
129 $values
130 );
131 };
132
133 if (is_object($process_locker)) {
134 $process_locker->executeUserTestResultUpdateLockOperation($callback);
135 } else {
136 $callback();
137 }
138
139 $this->updateStatusCache(
140 $attempt_result['user_id'],
141 $attempt_result['test_obj_id'],
142 [
143 'passed' => $result->isPassed(),
144 'failed' => $result->isFailed(),
145 'finished' => $status->isFinished(),
146 'percentage' => $result->getPercentage(),
147 ]
148 );
149
150 return $result;
151 }
152
153 public function getTestAttemptResult(int $active_id): ?AttemptResult
154 {
155 $result = $this->db->queryF(
156 "SELECT * FROM tst_pass_result WHERE active_fi = %s",
158 [$active_id]
159 );
160 return $this->toTestAttemptResult($this->db->fetchAssoc($result));
161 }
162
163 public function updateTestAttemptResult(
164 int $active_id,
165 int $attempt,
166 ?\ilAssQuestionProcessLocker $process_locker = null,
167 ?int $test_obj_id = null,
168 bool $update_result_cache_table = true
169 ): ?AttemptResult {
170 $test_result = $this->fetchTestResult($active_id, $attempt);
171 if (!$test_result) {
172 return null;
173 }
174
175 $result_object = $this->buildTestAttemptResultObject(
176 $active_id,
177 $test_result,
178 $test_obj_id
179 );
180
181 $callback = function () use ($result_object, $attempt) {
182 $this->db->replace(
183 'tst_pass_result',
184 [
185 'active_fi' => [\ilDBConstants::T_INTEGER, $result_object->getActiveId()],
186 'pass' => [\ilDBConstants::T_INTEGER, $attempt]
187 ],
188 [
189 'points' => [\ilDBConstants::T_FLOAT, $result_object->getReachedPoints()],
190 'maxpoints' => [\ilDBConstants::T_FLOAT, $result_object->getMaxPoints()],
191 'questioncount' => [\ilDBConstants::T_INTEGER, $result_object->getQuestionCount()],
192 'answeredquestions' => [\ilDBConstants::T_INTEGER, $result_object->getAnsweredQuestions()],
193 'workingtime' => [\ilDBConstants::T_INTEGER, $result_object->getWorkingTime()],
194 'tstamp' => [\ilDBConstants::T_INTEGER, time()],
195 'exam_id' => [\ilDBConstants::T_TEXT, $result_object->getExamId()],
196 'finalized_by' => [\ilDBConstants::T_TEXT, $result_object->getFinalizedBy()]
197 ]
198 );
199 };
200
201 if (is_object($process_locker)) {
202 $process_locker->executeUserPassResultUpdateLockOperation($callback);
203 } else {
204 $callback();
205 }
206
207 if ($update_result_cache_table) {
208 $this->updateTestResultCache($active_id, $process_locker);
209 }
210
211 return $result_object;
212 }
213
214 public function finalizeTestAttemptResult(int $active_id, int $attempt, StatusOfAttempt $status_of_attempt): void
215 {
216 if (!$status_of_attempt->isFinished()) {
217 throw new \RuntimeException('Status of attempt must be finished to finalize test attempt result');
218 }
219
220 $this->db->manipulateF(
221 'UPDATE tst_pass_result SET tstamp = %s, finalized_by = %s WHERE active_fi = %s AND pass = %s',
222 ['integer', 'text', 'integer', 'integer'],
223 [time(), $status_of_attempt->value, $active_id, $attempt]
224 );
225 }
226
227 private function fetchTestAttemptResult(int $active_id, int $attempt): ?array
228 {
229 return $this->db->fetchAssoc($this->db->queryF(
230 "SELECT tst_pass_result.*, tst_active.last_finished_pass, tst_active.user_fi AS user_id, tst_tests.test_id,
231 tst_tests.obj_fi AS test_obj_id, tst_pass_result.maxpoints AS max_points, points AS reached_points,
232 tst_result_cache.passed_once AS passed_once_before
233 FROM tst_pass_result
234 INNER JOIN tst_active ON tst_pass_result.active_fi = tst_active.active_id
235 INNER JOIN tst_tests ON tst_tests.test_id = tst_active.test_fi
236 LEFT JOIN tst_result_cache ON tst_result_cache.active_fi = tst_active.active_id
237 WHERE tst_pass_result.active_fi = %s AND tst_pass_result.pass = %s",
239 [$active_id, $attempt]
240 ));
241 }
242
243 private function buildTestResultObject(array $test_attempt_result_array): ParticipantResult
244 {
245 $test_attempt_result = $this->toParticipantResult($test_attempt_result_array);
246
247 $is_passed = $test_attempt_result->getAttempt() <= $test_attempt_result_array['last_finished_pass'] && $test_attempt_result->isPassed();
248 $passed_once_before = (bool) ($test_attempt_result_array['passed_once_before'] ?? false);
249 return $test_attempt_result->withPassedOnce($is_passed || $passed_once_before);
250 }
251
252 private function fetchTestResult(int $active_id, int $attempt): ?array
253 {
254 return $this->db->fetchAssoc($this->db->queryF(
255 'SELECT r.pass,' . PHP_EOL
256 . 'SUM(r.points) AS points,' . PHP_EOL
257 . 'COUNT(DISTINCT(r.question_fi)) answeredquestions,' . PHP_EOL
258 . 'pr.exam_id,' . PHP_EOL
259 . 'pr.finalized_by' . PHP_EOL
260 . 'FROM tst_test_result r' . PHP_EOL
261 . 'INNER JOIN tst_pass_result pr' . PHP_EOL
262 . 'ON r.active_fi = pr.active_fi AND r.pass = pr.pass' . PHP_EOL
263 . 'WHERE r.active_fi = %s AND r.pass = %s',
265 [$active_id, $attempt]
266 ));
267 }
268
269 private function buildTestAttemptResultObject(int $active_id, array $test_result, ?int $test_obj_id): AttemptResult
270 {
271 $test_result['active_fi'] = $active_id;
272 $test_attempt_result = $this->toTestAttemptResult($test_result);
273 $additional_data = $this->fetchAdditionalTestData($test_attempt_result->getActiveId(), $test_attempt_result->getAttempt());
274
275 return $test_attempt_result->withMaxPoints($additional_data['max_points'])
276 ->withQuestionCount($additional_data['question_count'])
277 ->withWorkingTime(
278 $this->fetchWorkingTime($test_attempt_result->getActiveId(), $test_attempt_result->getAttempt())
279 )
280 ->withExamId(
282 $test_attempt_result->getActiveId(),
283 $test_attempt_result->getAttempt(),
284 $test_obj_id
285 )
286 )
287 ->withTimestamp();
288 }
289
293 private function fetchAdditionalTestData(int $active_id, int $attempt): array
294 {
295 $qst_set_type_result = $this->db->queryF(
296 'SELECT tst_test_settings.question_set_type FROM tst_active' . PHP_EOL
297 . 'INNER JOIN tst_tests ON tst_active.test_fi = tst_tests.test_id' . PHP_EOL
298 . 'INNER JOIN tst_test_settings ON tst_tests.settings_id = tst_test_settings.id' . PHP_EOL
299 . 'WHERE tst_active.active_id = %s',
301 [$active_id]
302 );
303 $question_set_type = $qst_set_type_result->numRows() > 0
304 ? $this->db->fetchAssoc($qst_set_type_result)['question_set_type']
305 : '';
306
307 $result = match ($question_set_type) {
308 \ilObjTest::QUESTION_SET_TYPE_RANDOM => $this->db->queryF(
309 "SELECT tst_test_rnd_qst.pass, COUNT(tst_test_rnd_qst.question_fi) qcount, SUM(qpl_questions.points) qsum
310 FROM tst_test_rnd_qst, qpl_questions
311 WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
312 AND tst_test_rnd_qst.active_fi = %s AND pass = %s
313 GROUP BY tst_test_rnd_qst.active_fi, tst_test_rnd_qst.pass",
315 [$active_id, $attempt]
316 ),
317 \ilObjTest::QUESTION_SET_TYPE_FIXED => $this->db->queryF(
318 "SELECT COUNT(tst_test_question.question_fi) qcount, SUM(qpl_questions.points) qsum
319 FROM tst_test_question, qpl_questions, tst_active
320 WHERE tst_test_question.question_fi = qpl_questions.question_id
321 AND tst_test_question.test_fi = tst_active.test_fi AND tst_active.active_id = %s
322 GROUP BY tst_test_question.test_fi",
324 [$active_id]
325 ),
326 default => throw new \ilTestException('not supported question set type: ' . $question_set_type),
327 };
328
329 $row = $this->db->fetchAssoc($result);
330 return is_array($row)
331 ? ['question_count' => (int) $row['qcount'], 'max_points' => (float) $row['qsum']]
332 : ['question_count' => 0, 'max_points' => 0.0];
333 }
334
335 public function fetchWorkingTime(int $active_id, int $attempt): int
336 {
337 $result = $this->db->queryF(
338 "SELECT started, finished FROM tst_times WHERE active_fi = %s AND pass = %s ORDER BY started",
340 [$active_id, $attempt]
341 );
342
343 $time = 0;
344 while ($row = $this->db->fetchAssoc($result)) {
345 $time += (strtotime($row['finished']) - strtotime($row['started']));
346 }
347 return $time;
348 }
349
350 public function removeTestResults(array $active_ids, int $test_obj_id): void
351 {
352 $condition = $this->db->in('active_fi', $active_ids, false, \ilDBConstants::T_INTEGER);
353
354 $this->db->manipulate("DELETE FROM tst_test_result WHERE {$condition}");
355 $this->db->manipulate("DELETE FROM tst_pass_result WHERE {$condition}");
356
357 $user_ids = $this->db->fetchAll(
358 $this->db->query(
359 'SELECT user_fi FROM tst_active WHERE' . PHP_EOL
360 . $this->db->in('active_id', $active_ids, false, \ilDBConstants::T_INTEGER)
361 )
362 );
363 foreach ($user_ids as $row) {
364 $this->cache->delete($row['user_fi'] . ':' . $test_obj_id);
365 }
366 }
367
368 protected function lookupAttempt(int $active_id): ?int
369 {
370 return \ilObjTest::_getResultPass($active_id);
371 }
372
373
374 private function toParticipantResult(?array $row): ?ParticipantResult
375 {
376 if ($row === null) {
377 return null;
378 }
379
380 $mark = $this->marks_repository
381 ->getMarkSchemaFor($row['test_id'])
382 ->getMatchingMark($this->calculatePercentage($row) * 100);
383
384 return new ParticipantResult(
385 $row['active_fi'],
386 (int) $row['pass'],
387 $this->ensurePositive($row['max_points'] ?? 0.0),
388 $this->ensurePositive($row['reached_points'] ?? 0.0),
389 $mark,
390 (int) ($row['tstamp'] ?? -1),
391 (bool) ($row['passed_once'] ?? false),
392 );
393 }
394
395 private function toTestAttemptResult(?array $row): ?AttemptResult
396 {
397 if ($row === null) {
398 return null;
399 }
400
401 return new AttemptResult(
402 $row['active_fi'],
403 (int) $row['pass'],
404 $this->ensurePositive($row['maxpoints'] ?? 0.0),
405 $this->ensurePositive($row['points'] ?? 0.0),
406 (int) ($row['questioncount'] ?? 0),
407 (int) ($row['answeredquestions'] ?? 0),
408 (int) ($row['workingtime'] ?? 0),
409 (int) ($row['tstamp'] ?? -1),
410 $row['exam_id'] ?? '',
411 $row['finalized_by'] ?? '',
412 );
413 }
414
415 private function ensurePositive(mixed $value): float
416 {
417 return max(0.0, (float) $value);
418 }
419
423 private function updateStatusCache(int $user_id, int $test_obj_id, array $status): void
424 {
425 $this->cache->set($user_id . ':' . $test_obj_id, $status);
426 }
427
431 private function readOrQueryStatus(int $user_id, int $test_obj_id): ?array
432 {
433 $cached_status = $this->cache->get($user_id . ':' . $test_obj_id, $this->refinery->identity());
434 if ($cached_status !== null) {
435 return $cached_status;
436 }
437
438 $status = $this->db->fetchAssoc($this->db->queryF(
439 "SELECT tst_result_cache.passed, tst_result_cache.failed, (tst_active.last_finished_pass IS NOT NULL) AS finished,
440 tst_result_cache.reached_points, tst_result_cache.max_points
441 FROM tst_result_cache
442 INNER JOIN tst_active ON tst_active.active_id = tst_result_cache.active_fi
443 INNER JOIN tst_tests ON tst_tests.test_id = tst_active.test_fi
444 WHERE tst_active.user_fi = %s AND tst_tests.obj_fi = %s",
446 [$user_id, $test_obj_id]
447 ));
448 if ($status === null) {
449 return null;
450 }
451
452 $status['percentage'] = $this->calculatePercentage($status);
453 unset($status['reached_points'], $status['max_points']);
454
455 $this->updateStatusCache($user_id, $test_obj_id, $status);
456 return $status;
457 }
458
464 private function calculatePercentage(array $row): float
465 {
466 $max_points = $this->ensurePositive($row['max_points'] ?? 0.0);
467 $reached_points = $this->ensurePositive($row['reached_points'] ?? 0.0);
468
469 return $max_points > 0 ? $reached_points / $max_points : 0.0;
470 }
471}
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:252
readOrQueryStatus(int $user_id, int $test_obj_id)
Definition: Repository.php:431
reachedPercentage(int $a_usr_id, int $a_trigger_obj_id, float $min_threshold, float $max_threshold)
Definition: Repository.php:73
updateStatusCache(int $user_id, int $test_obj_id, array $status)
Definition: Repository.php:423
finalizeTestAttemptResult(int $active_id, int $attempt, StatusOfAttempt $status_of_attempt)
Definition: Repository.php:214
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:163
getPassedParticipants(int $test_obj_id)
Definition: Repository.php:45
fetchWorkingTime(int $active_id, int $attempt)
Definition: Repository.php:335
updateTestResultCache(int $active_id, ?\ilAssQuestionProcessLocker $process_locker=null)
Definition: Repository.php:97
fetchAdditionalTestData(int $active_id, int $attempt)
Definition: Repository.php:293
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:243
buildTestAttemptResultObject(int $active_id, array $test_result, ?int $test_obj_id)
Definition: Repository.php:269
removeTestResults(array $active_ids, int $test_obj_id)
Definition: Repository.php:350
fetchTestAttemptResult(int $active_id, int $attempt)
Definition: Repository.php:227
const QUESTION_SET_TYPE_RANDOM
const QUESTION_SET_TYPE_FIXED
static buildExamId($active_id, $pass, $test_obj_id=null)
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface ilDBInterface.