ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
ResultsExportExcel.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
25use PhpOffice\PhpSpreadsheet\Cell\DataType;
26
31{
32 public const EXCEL_BACKGROUND_COLOR = 'C0C0C0';
33
35 private array $aggregated_data;
40 private array $filter = [];
41
42 private \ilExcel $worksheet;
43
44 public function __construct(
45 private readonly \ilLanguage $lng,
46 private readonly \ilObjUser $current_user,
47 private readonly \ilObjTest $test_obj,
48 private readonly GeneralQuestionPropertiesRepository $question_repository,
49 private readonly string $filename = '',
50 private readonly bool $scoredonly = true
51 ) {
52 $this->user_date_format = $this->current_user->getDateTimeFormat();
53 $this->aggregated_data = $test_obj->getAggregatedResultsData();
54 $this->worksheet = new \ilExcel();
55 }
56
57 public function withFilterByActiveId(int $active_id): self
58 {
59 $clone = clone $this;
60 $clone->filter[\ilTestEvaluationData::FILTER_BY_ACTIVE_ID] = $active_id;
61 return $clone;
62 }
63
64 public function withAggregatedResultsPage(): self
65 {
66 $this->worksheet->addSheet($this->lng->txt('tst_results_aggregated'));
67
68 $current_row = $this->addAggregatedOverviewHeader(1);
69 $current_row = $this->addAggregatedOverviewContent($current_row);
70 $current_row = $this->addAggregatedQuestionsHeader($current_row);
71 $this->addAggregatedQuestionsContent($current_row);
72
73 return $this;
74 }
75
76 public function withResultsPage(): self
77 {
78 $this->worksheet->addSheet($this->lng->txt('tst_results'));
79 $this->addResultsContent($this->addResultsHeader());
80 return $this;
81 }
82
83 public function withUserPages(): self
84 {
85 $usersheet_titles = [];
86 foreach ($this->getCompleteData()->getParticipants() as $active_id => $user_data) {
87 $usersheet_titles = $this->addUserSheet(
88 $usersheet_titles,
89 $user_data->getName(),
90 $active_id
91 );
92
93 $passes = $this->getPassesDataFromUserData($user_data);
94 $current_row = 1;
95 foreach ($passes as $pass) {
96 $pass_nr = $pass->getPass();
97 $current_row = $this->addUserHeader(
98 $current_row,
99 $pass_nr,
100 $user_data->getName(),
101 $user_data->getScoredPass() === $pass_nr
102 );
103
104 $current_row = $this->addUserContent(
105 $current_row,
106 $user_data->getQuestions($pass_nr),
107 $pass,
108 $active_id
109 );
110
111 $current_row++;
112 }
113 }
114
115 return $this;
116 }
117
118 public function write(): ?string
119 {
121
122 $this->worksheet->setFormat(\ilExcel::FORMAT_XML);
123 $extension = '.' . strtolower(\ilExcel::FORMAT_XML);
124 if (!str_ends_with($path, $extension)) {
125 $path .= $extension;
126 }
127
128 $this->worksheet->writeToFile($path);
129 return $path;
130 }
131
135 public function deliver(): void
136 {
137 $this->worksheet->sendToClient($this->filename);
138 }
139
140 public function getContent(): \ilExcel
141 {
142 return $this->worksheet;
143 }
144
146 {
147 if ($this->complete_data === null) {
149 $filter_text = '';
150 if ($this->filter !== []) {
151 $filter_key = key($this->filter);
152 $filter_text = current($this->filter);
153 }
154 $this->complete_data = $this->test_obj->getCompleteEvaluationData($filter_key, $filter_text);
155 }
157 }
158
159 private function addAggregatedOverviewHeader(int $current_row): int
160 {
161 $col = 0;
162 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('result'));
163 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('value'));
164
165 $this->worksheet->setBold('A' . $current_row . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row);
166 $this->worksheet->setColors('A' . $current_row . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row, self::EXCEL_BACKGROUND_COLOR);
167 return ++$current_row;
168 }
169
170 private function addAggregatedOverviewContent(int $current_row): int
171 {
172 foreach ($this->aggregated_data['overview'] as $key => $value) {
173 $col = 0;
174 $this->worksheet->setCell($current_row, $col++, $this->lng->txt($key));
175 $this->worksheet->setCell($current_row, $col++, $value);
176 $current_row++;
177 }
178 return ++$current_row;
179 }
180
181 private function addAggregatedQuestionsHeader(int $current_row): int
182 {
183 $col = 0;
184 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('question_id'));
185 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('question_title'));
186 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('average_reached_points'));
187 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('points'));
188 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('percentage'));
189 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('number_of_answers'));
190
191 $this->worksheet->setBold('A' . $current_row . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row);
192 $this->worksheet->setColors('A' . $current_row . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row, self::EXCEL_BACKGROUND_COLOR);
193 return ++$current_row;
194 }
195
196 private function addAggregatedQuestionsContent(int $current_row): int
197 {
198 foreach ($this->aggregated_data['questions'] as $key => $value) {
199 $col = 0;
200 $this->worksheet->setCell($current_row, $col++, $key);
201 $this->worksheet->setCell($current_row, $col++, $value[0]);
202 $this->worksheet->setCell($current_row, $col++, $value[4]);
203 $this->worksheet->setCell($current_row, $col++, $value[5]);
204 $this->worksheet->setCell($current_row, $col++, $value[6]);
205 $this->worksheet->setCell($current_row, $col++, $value[3]);
206 $current_row++;
207 }
208 return $current_row;
209 }
210
214 private function addResultsHeader(): array
215 {
216 $col = 0;
217
218 if (!$this->scoredonly) {
219 $this->worksheet->setCell(1, $col++, $this->lng->txt('scored_pass'));
220 }
221
222 $this->worksheet->setCell(
223 1,
224 $col++,
225 $this->test_obj->getAnonymity() ? $this->lng->txt('counter') : $this->lng->txt('name')
226 );
227
228 if (!$this->test_obj->getAnonymity()) {
229 $this->worksheet->setCell(1, $col++, $this->lng->txt('login'));
230 $this->worksheet->setCell(1, $col++, $this->lng->txt('email'));
231 $this->worksheet->setCell(1, $col++, $this->lng->txt('matriculation'));
232 $this->worksheet->setCell(1, $col++, $this->lng->txt('gender'));
233 $this->worksheet->setCell(1, $col++, $this->lng->txt('street'));
234 $this->worksheet->setCell(1, $col++, $this->lng->txt('city'));
235 $this->worksheet->setCell(1, $col++, $this->lng->txt('zipcode'));
236 $this->worksheet->setCell(1, $col++, $this->lng->txt('country'));
237 $this->worksheet->setCell(1, $col++, $this->lng->txt('institution'));
238 $this->worksheet->setCell(1, $col++, $this->lng->txt('department'));
239 }
240
241 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_firstvisit'));
242 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_lastvisit'));
243 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_total_timeontask'));
244
245 if ($this->test_obj->isShowExamIdInTestResultsEnabled()) {
246 $this->worksheet->setCell(1, $col++, $this->lng->txt('exam_id_label'));
247 }
248
249 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_resultspoints'));
250 $this->worksheet->setCell(1, $col++, $this->lng->txt('maximum_points'));
251 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_resultsmarks'));
252 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_qmax'));
253 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_qworkedthrough'));
254 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_pworkedthrough'));
255 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_timeontask'));
256 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_atimeofwork'));
257 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_stat_result_rank_participant'));
258 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_tbl_col_started_passes'));
259 $this->worksheet->setCell(1, $col++, $this->lng->txt('tst_tbl_col_finished_passes'));
260 $this->worksheet->setCell(1, $col++, $this->lng->txt('scored_pass'));
261 $this->worksheet->setCell(1, $col++, $this->lng->txt('pass'));
262
263 $question_cols = [];
264 foreach ($this->question_repository->getForParentObjectId($this->test_obj->getId()) as $question_properties) {
265 $question_cols[$question_properties->getQuestionId()] = $col;
266 $this->worksheet->setCell(1, $col++, $question_properties->getTitle());
267 }
268
269 $this->worksheet->setBold('A1:' . $this->worksheet->getColumnCoord($col - 1) . '1');
270 $this->worksheet->setColors('A1:' . $this->worksheet->getColumnCoord($col - 1) . '1', self::EXCEL_BACKGROUND_COLOR);
271
272 return $question_cols;
273 }
274
275 private function addResultsContent(array $cols_for_question_ids): void
276 {
277 $current_row = 2;
278 foreach ($this->getCompleteData()->getParticipants() as $active_id => $user_data) {
279 $userfields = $this->getUserFieldsForUserID($user_data->getUserID());
280 for ($test_attempt = 0; $test_attempt <= $user_data->getLastPass(); $test_attempt++) {
281 $finishdate = \ilObjTest::lookupPassResultsUpdateTimestamp($active_id, $test_attempt);
282 $is_scored_attempt = $test_attempt === $user_data->getScoredPass();
283 if ($finishdate < 1
284 || $this->scoredonly && !$is_scored_attempt) {
285 continue;
286 }
287
289 $test_attempt_data = $user_data->getPass($test_attempt);
290 $col = 0;
291
292 if (!$this->scoredonly) {
293 $this->worksheet->setCell(
294 $current_row,
295 $col++,
296 $is_scored_attempt ? 'x' : ''
297 );
298 }
299
300 $this->worksheet->setCell(
301 $current_row,
302 $col++,
303 $this->test_obj->getAnonymity() ? $current_row - 1 : $user_data->getName()
304 );
305 if (!$this->test_obj->getAnonymity()) {
306 $this->worksheet->setCell($current_row, $col++, $user_data->getLogin());
307 $this->worksheet->setCell($current_row, $col++, $userfields['email'] ?? '');
308 $this->worksheet->setCell($current_row, $col++, $userfields['matriculation'] ?? '');
309 $this->worksheet->setCell($current_row, $col++, isset($userfields['gender']) && $userfields['gender'] !== ''
310 ? $this->lng->txt('gender_' . $userfields['gender'])
311 : '');
312 $this->worksheet->setCell($current_row, $col++, $userfields['street'] ?? '');
313 $this->worksheet->setCell($current_row, $col++, $userfields['city'] ?? '');
314 $this->worksheet->setCell($current_row, $col++, $userfields['zipcode'] ?? '');
315 $this->worksheet->setCell($current_row, $col++, $userfields['country'] ?? '');
316 $this->worksheet->setCell($current_row, $col++, $userfields['institution'] ?? '');
317 $this->worksheet->setCell($current_row, $col++, $userfields['departement'] ?? '');
318 }
319
320 $this->worksheet->setCell($current_row, $col++, $this->convertToUserDateFormat($user_data->getFirstVisit()));
321 $this->worksheet->setCell($current_row, $col++, $this->convertToUserDateFormat($user_data->getLastVisit()));
322 $this->worksheet->setCell($current_row, $col++, $this->secondsToHoursMinutesSecondsString($user_data->getTimeOnTask()));
323
324 if ($this->test_obj->isShowExamIdInTestResultsEnabled()) {
325 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getExamId());
326 }
327
328 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getReachedPoints());
329 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getMaxpoints());
330 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getMark()?->getShortName() ?? '');
331 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getQuestionCount());
332 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getNrOfAnsweredQuestions());
333 $this->worksheet->setCell($current_row, $col++, $test_attempt_data->getReachedPointsInPercent());
334 $this->worksheet->setCell($current_row, $col++, $this->secondsToHoursMinutesSecondsString($test_attempt_data->getWorkingTime()));
335 $this->worksheet->setCell($current_row, $col++, $this->secondsToHoursMinutesSecondsString(
336 $test_attempt_data->getAnsweredQuestionCount() !== 0 ? intdiv($test_attempt_data->getWorkingTime(), $test_attempt_data->getAnsweredQuestionCount()) : 0
337 ));
338
339 $ranking = '';
340 if ($is_scored_attempt) {
341 $ranking = $this->getCompleteData()->getStatistics()->rank(
342 $test_attempt_data->getReachedPoints()
343 ) ?? '';
344 }
345 $this->worksheet->setCell(
346 $current_row,
347 $col++,
348 $ranking
349 );
350
351 $this->worksheet->setCell($current_row, $col++, $user_data->getPassCount());
352 $this->worksheet->setCell($current_row, $col++, $user_data->getFinishedPasses());
353 if ($this->test_obj->getPassScoring() === \ilObjTest::SCORE_BEST_PASS) {
354 $this->worksheet->setCell($current_row, $col++, $user_data->getBestPass() + 1);
355 } else {
356 $this->worksheet->setCell($current_row, $col++, $user_data->getLastPass() + 1);
357 }
358 $this->worksheet->setCell($current_row, $col++, $test_attempt + 1);
359
360 foreach ($test_attempt_data->getAnsweredQuestions() as $question) {
361 $this->worksheet->setCell(
362 $current_row,
363 $cols_for_question_ids[$question['id']],
364 $question['reached']
365 );
366 }
367
368 $current_row++;
369 }
370 }
371 }
372
373 private function addUserSheet(
374 array $usersheet_titles,
375 string $user_name,
376 int $active_id
377 ): array {
378 $username = mb_substr(
379 $user_name !== '' ? $user_name : "ID {$active_id}",
380 0,
381 26
382 );
383 $username_to_lower = strtolower($username);
384 if (array_key_exists($username_to_lower, $usersheet_titles)) {
385 $username .= ' (' . ++$usersheet_titles[$username_to_lower] . ')';
386 } else {
387 $usersheet_titles[$username_to_lower] = 0;
388 }
389
390 $this->worksheet->addSheet($username);
391 return $usersheet_titles;
392 }
393
394 private function addUserHeader(
395 int $current_row,
396 int $test_attempt,
397 string $user_name,
398 bool $is_scored_test_attempt
399 ): int {
400 $title = sprintf(
401 $this->lng->txt('tst_result_user_name_pass'),
402 $test_attempt + 1,
403 $user_name
404 );
405
406 if (!$this->scoredonly && $is_scored_test_attempt) {
407 $scoring_type = $this->test_obj->getPassScoring()
408 ? $this->lng->txt('tst_pass_scoring_best')
409 : $this->lng->txt('tst_pass_scoring_last');
410 $title .= " - {$this->lng->txt('exp_scored_test_attempt')} ({$scoring_type})";
411 }
412
413 $this->worksheet->setCell($current_row, 0, $title);
414
415 $current_row++;
416
417 $col = 0;
418 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('title'));
419 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('question_type'));
420 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('answer'));
421 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('correct_answers'));
422 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('variables'));
423 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('tst_reached_points'));
424 $this->worksheet->setCell($current_row, $col++, $this->lng->txt('tst_maximum_points'));
425
426 $this->worksheet->mergeCells('A' . $current_row - 1 . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row - 1);
427 $this->worksheet->setBold('A' . $current_row - 1 . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row);
428 $this->worksheet->setColors('A' . $current_row - 1 . ':' . $this->worksheet->getColumnCoord($col - 1) . $current_row, self::EXCEL_BACKGROUND_COLOR);
429
430 return ++$current_row;
431 }
432
433 private function addUserContent(
434 int $current_row,
435 ?array $questions,
436 \ilTestEvaluationPassData $test_attempt,
437 int $active_id
438 ): int {
439 if ($questions === null) {
440 return $current_row;
441 }
442
443 usort(
444 $questions,
445 static fn(array $a, array $b): int => $a['sequence'] - $b['sequence']
446 );
447
448 foreach ($questions as $question) {
449 $question_id = (int) $question['id'];
450
451 $question_obj = \assQuestion::instantiateQuestion($question_id);
452
453 $answers = '';
454 $question_from_answered_questions = $test_attempt->getAnsweredQuestionByQuestionId($question_id);
455 if ($question_from_answered_questions !== null
456 && $question_from_answered_questions['isAnswered']) {
457 $answers = $question_obj->getSolutionForTextOutput($active_id, $test_attempt->getPass());
458 }
459
460 if (is_array($answers)) {
461 $answers = implode("\n", $answers);
462 }
463
464 $disable_strip_tags_for_answers = $question_obj instanceof \assTextQuestion
465 && $this->test_obj->getGlobalSettings()->getExportEssayQuestionsAsHtml();
466
467 $correct_answers = $question_obj->getCorrectSolutionForTextOutput($active_id, $test_attempt->getPass());
468 if (is_array($correct_answers)) {
469 $correct_answers = implode("\n", $correct_answers);
470 }
471
472 $col = 0;
473 $this->worksheet->setCell($current_row, $col++, $question_obj->getTitle());
474 $this->worksheet->setCell($current_row, $col++, $this->lng->txt($question_obj->getQuestionType()));
475 $this->worksheet->setCell($current_row, $col++, $answers, DataType::TYPE_STRING, $disable_strip_tags_for_answers);
476 $this->worksheet->setCell($current_row, $col++, $correct_answers);
477 $this->worksheet->setCell($current_row, $col++, implode(', ', $question_obj->getVariablesAsTextArray($active_id, $test_attempt->getPass())));
478 $this->worksheet->setCell($current_row, $col++, $test_attempt->getAnsweredQuestionByQuestionId($question_id)['reached'] ?? 0);
479 $this->worksheet->setCell($current_row, $col++, $question['points']);
480
481 $current_row++;
482 }
483
484 return $current_row;
485 }
486
490 private function getPassesDataFromUserData(\ilTestEvaluationUserData $user_data): array
491 {
492 if ($this->scoredonly) {
493 return [$user_data->getScoredPassObject()];
494 }
495 return $user_data->getPasses();
496 }
497
498 private function getUserFieldsForUserID(?int $user_id): array
499 {
500 if ($user_id === null) {
501 return [];
502 }
503 return $userfields = \ilObjUser::_lookupFields($user_id);
504 }
505
506 private function secondsToHoursMinutesSecondsString(int $seconds): string
507 {
508 $diff_hours = floor($seconds / 3600);
509 $seconds -= $diff_hours * 3600;
510 $diff_minutes = floor($seconds / 60);
511 $seconds -= $diff_minutes * 60;
512 return sprintf('%02d:%02d:%02d', $diff_hours, $diff_minutes, $seconds);
513 }
514
515 private function convertToUserDateFormat(?\DateTimeImmutable $date_time): string
516 {
517 if ($date_time === null) {
518 return '';
519 }
520
521 return $date_time
522 ->setTimezone(new \DateTimeZone($this->current_user->getTimeZone()))
523 ->format($this->user_date_format->toString());
524 }
525}
$filename
Definition: buildRTE.php:78
A Date Format provides a format definition akin to PHP's date formatting options, but stores the sing...
Definition: DateFormat.php:27
addUserContent(int $current_row, ?array $questions, \ilTestEvaluationPassData $test_attempt, int $active_id)
addUserSheet(array $usersheet_titles, string $user_name, int $active_id)
convertToUserDateFormat(?\DateTimeImmutable $date_time)
addUserHeader(int $current_row, int $test_attempt, string $user_name, bool $is_scored_test_attempt)
__construct(private readonly \ilLanguage $lng, private readonly \ilObjUser $current_user, private readonly \ilObjTest $test_obj, private readonly GeneralQuestionPropertiesRepository $question_repository, private readonly string $filename='', private readonly bool $scoredonly=true)
getPassesDataFromUserData(\ilTestEvaluationUserData $user_data)
static instantiateQuestion(int $question_id)
const FORMAT_XML
static ilTempnam(?string $a_temp_path=null)
Returns a unique and non existing Path for e temporary file or directory.
language handling
const SCORE_BEST_PASS
static lookupPassResultsUpdateTimestamp($active_id, $pass)
User class.
static _lookupFields(int $a_user_id)
getScoredPassObject()
returns the object of class ilTestEvaluationPassData that relates to the the scored test pass (best p...
$path
Definition: ltiservices.php:30
filter(string $filter_id, $class_path, string $cmd, bool $activated=true, bool $expanded=true)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
global $lng
Definition: privfeed.php:31
if(!file_exists('../ilias.ini.php'))