ILIAS  trunk Revision v11.0_alpha-1723-g8e69f309bab
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
ResultsExportExcel.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
21 namespace ILIAS\Test\ExportImport;
22 
26 
30 class ResultsExportExcel implements Exporter
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  }
156  return $this->complete_data;
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 }
convertToUserDateFormat(?\DateTimeImmutable $date_time)
static lookupPassResultsUpdateTimestamp($active_id, $pass)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getPassesDataFromUserData(\ilTestEvaluationUserData $user_data)
$path
Definition: ltiservices.php:29
getScoredPassObject()
returns the object of class ilTestEvaluationPassData that relates to the the scored test pass (best p...
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
static instantiateQuestion(int $question_id)
A Date Format provides a format definition akin to PHP&#39;s date formatting options, but stores the sing...
Definition: DateFormat.php:26
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)
static _lookupFields(int $a_user_id)
lookup fields (deprecated; use more specific methods instead)
$filename
Definition: buildRTE.php:78
static ilTempnam(?string $a_temp_path=null)
Returns a unique and non existing Path for e temporary file or directory.
addUserSheet(array $usersheet_titles, string $user_name, int $active_id)
const FORMAT_XML
global $lng
Definition: privfeed.php:31
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
filter(string $filter_id, $class_path, string $cmd, bool $activated=true, bool $expanded=true)
const SCORE_BEST_PASS
addUserContent(int $current_row, ?array $questions, \ilTestEvaluationPassData $test_attempt, int $active_id)