ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.assErrorText.php
Go to the documentation of this file.
1 <?php
2 
19 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
20 
34 {
35  protected const ERROR_TYPE_WORD = 1;
36  protected const ERROR_TYPE_PASSAGE = 2;
37  protected const DEFAULT_TEXT_SIZE = 100.0;
38  protected const ERROR_MAX_LENGTH = 150;
39 
40  protected const PARAGRAPH_SPLIT_REGEXP = '/[\n\r]+/';
41  protected const WORD_SPLIT_REGEXP = '/\s+/';
42  protected const FIND_PUNCTUATION_REGEXP = '/\p{P}/';
43  protected const ERROR_WORD_MARKER = '#';
44  protected const ERROR_PARAGRAPH_DELIMITERS = [
45  'start' => '((',
46  'end' => '))'
47  ];
48 
49  protected string $errortext = '';
50  protected array $parsed_errortext = [];
52  protected array $errordata = [];
53  protected float $textsize;
54  protected ?float $points_wrong;
55 
65  public function __construct(
66  $title = '',
67  $comment = '',
68  $author = '',
69  $owner = -1,
70  $question = ''
71  ) {
73  $this->textsize = self::DEFAULT_TEXT_SIZE;
74  }
75 
81  public function isComplete(): bool
82  {
83  if (mb_strlen($this->title)
84  && ($this->author)
85  && ($this->question)
86  && ($this->getMaximumPoints() > 0)) {
87  return true;
88  } else {
89  return false;
90  }
91  }
92 
97  public function saveToDb($original_id = ""): void
98  {
99  if ($original_id == '') {
100  $this->saveQuestionDataToDb();
101  } else {
103  }
104 
107  parent::saveToDb();
108  }
109 
110  public function saveAnswerSpecificDataToDb()
111  {
112  $this->db->manipulateF(
113  "DELETE FROM qpl_a_errortext WHERE question_fi = %s",
114  ['integer'],
115  [$this->getId()]
116  );
117 
118  $sequence = 0;
119  foreach ($this->errordata as $error) {
120  $next_id = $this->db->nextId('qpl_a_errortext');
121  $this->db->manipulateF(
122  "INSERT INTO qpl_a_errortext (answer_id, question_fi, text_wrong, text_correct, points, sequence, position) VALUES (%s, %s, %s, %s, %s, %s, %s)",
123  ['integer', 'integer', 'text', 'text', 'float', 'integer', 'integer'],
124  [
125  $next_id,
126  $this->getId(),
127  $error->getTextWrong(),
128  $error->getTextCorrect(),
129  $error->getPoints(),
130  $sequence++,
131  $error->getPosition()
132  ]
133  );
134  }
135  }
136 
143  {
144  $this->db->manipulateF(
145  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
146  ["integer"],
147  [$this->getId()]
148  );
149 
150  $this->db->manipulateF(
151  "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, errortext, parsed_errortext, textsize, points_wrong) VALUES (%s, %s, %s, %s, %s)",
152  ["integer", "text", "text", "float", "float"],
153  [
154  $this->getId(),
155  $this->getErrorText(),
156  json_encode($this->getParsedErrorText()),
157  $this->getTextSize(),
158  $this->getPointsWrong()
159  ]
160  );
161  }
162 
169  public function loadFromDb($question_id): void
170  {
171  $db_question = $this->db->queryF(
172  "SELECT qpl_questions.*, " . $this->getAdditionalTableName() . ".* FROM qpl_questions LEFT JOIN " . $this->getAdditionalTableName() . " ON " . $this->getAdditionalTableName() . ".question_fi = qpl_questions.question_id WHERE qpl_questions.question_id = %s",
173  ["integer"],
174  [$question_id]
175  );
176  if ($db_question->numRows() === 1) {
177  $data = $this->db->fetchAssoc($db_question);
178  $this->setId($question_id);
179  $this->setObjId($data["obj_fi"]);
180  $this->setTitle((string) $data["title"]);
181  $this->setComment((string) $data["description"]);
182  $this->setOriginalId($data["original_id"]);
183  $this->setNrOfTries($data['nr_of_tries']);
184  $this->setAuthor($data["author"]);
185  $this->setPoints($data["points"]);
186  $this->setOwner($data["owner"]);
187  $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data["question_text"], 1));
188  $this->setErrorText((string) $data["errortext"]);
189  $this->setParsedErrorText(json_decode($data['parsed_errortext'] ?? json_encode([]), true));
190  $this->setTextSize($data["textsize"]);
191  $this->setPointsWrong($data["points_wrong"]);
192 
193  try {
194  $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
197  }
198 
199  try {
200  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
201  } catch (ilTestQuestionPoolException $e) {
202  }
203  }
204 
205  $db_error_text = $this->db->queryF(
206  "SELECT * FROM qpl_a_errortext WHERE question_fi = %s ORDER BY sequence ASC",
207  ['integer'],
208  [$question_id]
209  );
210 
211  if ($db_error_text->numRows() > 0) {
212  while ($data = $this->db->fetchAssoc($db_error_text)) {
213  $this->errordata[] = new assAnswerErrorText(
214  (string) $data['text_wrong'],
215  (string) $data['text_correct'],
216  (float) $data['points'],
217  $data['position']
218  );
219  }
220  }
221 
223 
224  parent::loadFromDb($question_id);
225  }
226 
227  private function correctDataAfterParserUpdate(): void
228  {
229  if ($this->getErrorText() === '') {
230  return;
231  }
232  $needs_finalizing = false;
233  if ($this->getParsedErrorText() === []) {
234  $needs_finalizing = true;
235  $this->parseErrorText();
236  }
237 
238  if (isset($this->errordata[0])
239  && $this->errordata[0]->getPosition() === null) {
240  foreach ($this->errordata as $key => $error) {
241  $this->errordata[$key] = $this->addPositionToErrorAnswer($error);
242  }
244  }
245 
246  if ($needs_finalizing) {
249  }
250  }
251 
255  public function duplicate(bool $for_test = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int
256  {
257  if ($this->id <= 0) {
258  // The question has not been saved. It cannot be duplicated
259  return -1;
260  }
261  // duplicate the question in database
262  $this_id = $this->getId();
263  $thisObjId = $this->getObjId();
264 
265  $clone = $this;
266 
267  $original_id = $this->questioninfo->getOriginalId($this->id);
268  $clone->id = -1;
269 
270  if ((int) $testObjId > 0) {
271  $clone->setObjId($testObjId);
272  }
273 
274  if ($title) {
275  $clone->setTitle($title);
276  }
277 
278  if ($author) {
279  $clone->setAuthor($author);
280  }
281  if ($owner) {
282  $clone->setOwner($owner);
283  }
284 
285  if ($for_test) {
286  $clone->saveToDb($original_id);
287  } else {
288  $clone->saveToDb();
289  }
290  // copy question page content
291  $clone->copyPageOfQuestion($this_id);
292  // copy XHTML media objects
293  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
294 
295  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
296  return $clone->id;
297  }
298 
302  public function copyObject($target_questionpool_id, $title = ""): int
303  {
304  if ($this->getId() <= 0) {
305  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
306  }
307  // duplicate the question in database
308 
309  $thisId = $this->getId();
310  $thisObjId = $this->getObjId();
311 
312  $clone = $this;
313 
314  $original_id = $this->questioninfo->getOriginalId($this->id);
315  $clone->id = -1;
316  $clone->setObjId($target_questionpool_id);
317  if ($title) {
318  $clone->setTitle($title);
319  }
320  $clone->saveToDb();
321 
322  // copy question page content
323  $clone->copyPageOfQuestion($original_id);
324  // copy XHTML media objects
325  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
326 
327  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
328 
329  return $clone->id;
330  }
331 
332  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
333  {
334  if ($this->getId() <= 0) {
335  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
336  }
337 
338  $sourceQuestionId = $this->id;
339  $sourceParentId = $this->getObjId();
340 
341  // duplicate the question in database
342  $clone = $this;
343  $clone->id = -1;
344 
345  $clone->setObjId($targetParentId);
346 
347  if ($targetQuestionTitle) {
348  $clone->setTitle($targetQuestionTitle);
349  }
350 
351  $clone->saveToDb();
352  // copy question page content
353  $clone->copyPageOfQuestion($sourceQuestionId);
354  // copy XHTML media objects
355  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
356 
357  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
358 
359  return $clone->id;
360  }
361 
367  public function getMaximumPoints(): float
368  {
369  $maxpoints = 0.0;
370  foreach ($this->errordata as $error) {
371  if ($error->getPoints() > 0) {
372  $maxpoints += $error->getPoints();
373  }
374  }
375  return $maxpoints;
376  }
377 
388  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float
389  {
390  if ($returndetails) {
391  throw new ilTestException('return details not implemented for ' . __METHOD__);
392  }
393 
394  /* First get the positions which were selected by the user. */
395  $positions = [];
396  if (is_null($pass)) {
397  $pass = $this->getSolutionMaxPass($active_id);
398  }
399  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
400 
401  while ($row = $this->db->fetchAssoc($result)) {
402  $positions[] = $row['value1'];
403  }
404  $points = $this->getPointsForSelectedPositions($positions);
405  return $points;
406  }
407 
409  {
410  $reached_points = $this->getPointsForSelectedPositions($preview_session->getParticipantsSolution() ?? []);
411  $reached_points = $this->deductHintPointsFromReachedPoints($preview_session, $reached_points);
412  return $this->ensureNonNegativePoints($reached_points);
413  }
414 
423  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
424  {
425  if (is_null($pass)) {
426  $pass = ilObjTest::_getPass($active_id);
427  }
428 
429  $selected = $this->getAnswersFromRequest();
430  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
431  function () use ($selected, $active_id, $pass, $authorized) {
432  $this->removeCurrentSolution($active_id, $pass, $authorized);
433 
434  foreach ($selected as $position) {
435  $this->saveCurrentSolution($active_id, $pass, $position, null, $authorized);
436  }
437  }
438  );
439 
441  $this->logUserAction($selected !== [], (int) $active_id);
442  }
443 
444  return true;
445  }
446 
447  public function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
448  {
449  $selection = $this->getAnswersFromRequest();
450  $previewSession->setParticipantsSolution($selection);
451  }
452 
453  private function logUserAction(bool $user_entered_values, int $active_id): void
454  {
455  $log_text = $this->lng->txtlng(
456  "assessment",
457  $user_entered_values ? 'log_user_entered_values' : 'log_user_not_entered_values',
459  );
460  assQuestion::logAction($log_text, $active_id, $this->getId());
461  }
462 
463  private function getAnswersFromRequest(): array
464  {
465  if (mb_strlen($_POST["qst_" . $this->getId()])) {
466  return explode(',', $_POST["qst_{$this->getId()}"]);
467  }
468 
469  return [];
470  }
471 
472  public function getQuestionType(): string
473  {
474  return 'assErrorText';
475  }
476 
477  public function getAdditionalTableName(): string
478  {
479  return 'qpl_qst_errortext';
480  }
481 
482  public function getAnswerTableName(): string
483  {
484  return 'qpl_a_errortext';
485  }
486 
490  public function setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass): int
491  {
492  parent::setExportDetailsXLSX($worksheet, $startrow, $col, $active_id, $pass);
493 
494  $i = 0;
495  $selections = [];
496  $solutions = $this->getSolutionValues($active_id, $pass);
497  if (is_array($solutions)) {
498  foreach ($solutions as $solution) {
499  $selections[] = $solution['value1'];
500  }
501  }
502  $errortext = $this->createErrorTextExport($selections);
503  $i++;
504  $worksheet->setCell($startrow + $i, $col + 2, $errortext);
505  $i++;
506 
507  return $startrow + $i + 1;
508  }
509 
510  public function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array
511  {
512  $import = new assErrorTextImport($this);
513  return $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping);
514  }
515 
516  public function toXML($a_include_header = true, $a_include_binary = true, $a_shuffle = false, $test_output = false, $force_image_references = false): string
517  {
518  $export = new assErrorTextExport($this);
519  return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
520  }
521 
522  public function setErrorsFromParsedErrorText(): void
523  {
524  $current_error_data = $this->getErrorData();
525  $this->errordata = [];
526 
527  $has_too_long_errors = false;
528  foreach ($this->getParsedErrorText() as $paragraph) {
529  foreach ($paragraph as $position => $word) {
530  if ($word['error_type'] === 'in_passage'
531  || $word['error_type'] === 'passage_end'
532  || $word['error_type'] === 'none') {
533  continue;
534  }
535 
536  $text_wrong = $word['text_wrong'];
537  if (mb_strlen($text_wrong) > self::ERROR_MAX_LENGTH) {
538  $has_too_long_errors = true;
539  continue;
540  }
541 
542  list($text_correct, $points) =
543  $this->getAdditionalInformationFromExistingErrorDataByErrorText($current_error_data, $text_wrong);
544  $this->errordata[] = new assAnswerErrorText($text_wrong, $text_correct, $points, $position);
545  }
546  }
547 
548  if ($has_too_long_errors) {
549  $this->tpl->setOnScreenMessage(
550  'failure',
551  $this->lng->txt('qst_error_text_too_long')
552  );
553  }
554  }
555 
557  {
558  foreach ($this->getParsedErrorText() as $paragraph) {
559  foreach ($paragraph as $position => $word) {
560  if (isset($word['text_wrong'])
561  && ($word['text_wrong'] === $error->getTextWrong()
562  || mb_substr($word['text_wrong'], 0, -1) === $error->getTextWrong()
563  && preg_match(self::FIND_PUNCTUATION_REGEXP, mb_substr($word['text_wrong'], -1)) === 1)
564  && !array_key_exists($position, $this->generateArrayByPositionFromErrorData())
565  ) {
566  return $error->withPosition($position);
567  }
568  }
569  }
570 
571  return $error;
572  }
573 
574  private function completeParsedErrorTextFromErrorData(): void
575  {
576  foreach ($this->errordata as $error) {
577  $position = $error->getPosition();
578  foreach ($this->getParsedErrorText() as $key => $paragraph) {
579  if (array_key_exists($position, $paragraph)) {
580  $this->parsed_errortext[$key][$position]['text_correct'] =
581  $error->getTextCorrect();
582  $this->parsed_errortext[$key][$position]['points'] =
583  $error->getPoints();
584  break;
585  }
586  }
587  }
588  }
589 
594  public function setErrorData(array $errors): void
595  {
596  $this->errordata = [];
597 
598  foreach ($errors as $error) {
599  $answer = $this->addPositionToErrorAnswer($error);
600  $this->errordata[] = $answer;
601  }
603  }
604 
605  public function removeErrorDataWithoutPosition(): void
606  {
607  foreach ($this->getErrorData() as $index => $error) {
608  if ($error->getPosition() === null) {
609  unset($this->errordata[$index]);
610  }
611  }
612  $this->errordata = array_values($this->errordata);
613  }
614 
621  array $current_error_data,
622  string $text_wrong
623  ): array {
624  foreach ($current_error_data as $answer_object) {
625  if (strcmp($answer_object->getTextWrong(), $text_wrong) === 0) {
626  return[
627  $answer_object->getTextCorrect(),
628  $answer_object->getPoints()
629  ];
630  }
631  }
632  return ['', 0.0];
633  }
634 
635  public function assembleErrorTextOutput(
636  array $selections,
637  bool $graphical_output = false,
638  bool $show_correct_solution = false,
639  bool $use_link_tags = true,
640  array $correctness_icons = []
641  ): string {
642  $output_array = [];
643  foreach ($this->getParsedErrorText() as $paragraph) {
644  $array_reduce_function = fn(?string $carry, int $position)
645  => $carry . $this->generateOutputStringFromPosition(
646  $position,
647  $selections,
648  $paragraph,
649  $graphical_output,
650  $show_correct_solution,
651  $use_link_tags,
652  $correctness_icons
653  );
654  $output_array[] = '<p>' . trim(array_reduce(array_keys($paragraph), $array_reduce_function)) . '</p>';
655  }
656 
657  return implode("\n", $output_array);
658  }
659 
661  int $position,
662  array $selections,
663  array $paragraph,
664  bool $graphical_output,
665  bool $show_correct_solution,
666  bool $use_link_tags,
667  array $correctness_icons
668  ): string {
669  $text = $this->getTextForPosition($position, $paragraph, $show_correct_solution);
670  if ($text === '') {
671  return '';
672  }
673  $class = $this->getClassForPosition($position, $show_correct_solution, $selections);
674  $img = $this->getCorrectnessIconForPosition(
675  $position,
676  $graphical_output,
677  $selections,
678  $correctness_icons
679  );
680 
681  return ' ' . $this->getErrorTokenHtml($text, $class, $use_link_tags) . $img;
682  }
683 
684  private function getTextForPosition(
685  int $position,
686  array $paragraph,
687  bool $show_correct_solution
688  ): string {
689  $v = $paragraph[$position];
690  if ($show_correct_solution === true
691  && ($v['error_type'] === 'in_passage'
692  || $v['error_type'] === 'passage_end')) {
693  return '';
694  }
695  if ($show_correct_solution
696  && ($v['error_type'] === 'passage_start'
697  || $v['error_type'] === 'word')) {
698  return $v['text_correct'] ?? '';
699  }
700 
701  return $v['text'];
702  }
703 
704  private function getClassForPosition(
705  int $position,
706  bool $show_correct_solution,
707  array $selections
708  ): string {
709  if ($show_correct_solution !== true
710  && in_array($position, $selections['user'])) {
711  return 'ilc_qetitem_ErrorTextSelected';
712  }
713 
714  if ($show_correct_solution === true
715  && in_array($position, $selections['best'])) {
716  return 'ilc_qetitem_ErrorTextSelected';
717  }
718 
719  return 'ilc_qetitem_ErrorTextItem';
720  }
721 
723  int $position,
724  bool $graphical_output,
725  array $selections,
726  array $correctness_icons
727  ): string {
728  if ($graphical_output === true
729  && (in_array($position, $selections['user']) && !in_array($position, $selections['best'])
730  || !in_array($position, $selections['user']) && in_array($position, $selections['best']))) {
731  return $correctness_icons['not_correct'];
732  }
733 
734  if ($graphical_output === true
735  && in_array($position, $selections['user']) && in_array($position, $selections['best'])) {
736  return $correctness_icons['correct'];
737  }
738 
739  return '';
740  }
741 
742  public function createErrorTextExport(array $selections): string
743  {
744  if (!is_array($selections)) {
745  $selections = [];
746  }
747 
748  foreach ($this->getParsedErrorText() as $paragraph) {
749  $array_reduce_function = function ($carry, $k) use ($paragraph, $selections) {
750  $text = $paragraph[$k]['text'];
751  if (in_array($k, $selections)) {
752  $text = self::ERROR_WORD_MARKER . $paragraph[$k]['text'] . self::ERROR_WORD_MARKER;
753  }
754  return $carry . ' ' . $text;
755  };
756  $output_array[] = trim(array_reduce(array_keys($paragraph), $array_reduce_function));
757  }
758  return implode("\n", $output_array);
759  }
760 
761  public function getBestSelection($withPositivePointsOnly = true): array
762  {
763  $positions_array = $this->generateArrayByPositionFromErrorData();
764  $selections = [];
765  foreach ($positions_array as $position => $position_data) {
766  if ($position === ''
767  || $withPositivePointsOnly && $position_data['points'] <= 0) {
768  continue;
769  }
770 
771  $selections[] = $position;
772  if ($position_data['length'] > 1) {
773  for ($i = 1;$i < $position_data['length'];$i++) {
774  $selections[] = $position + $i;
775  }
776  }
777  }
778 
779  return $selections;
780  }
781 
786  protected function getPointsForSelectedPositions(array $selected_word_positions): float
787  {
788  $points = 0;
789  $correct_positions = $this->generateArrayByPositionFromErrorData();
790 
791  foreach ($correct_positions as $correct_position => $correct_position_data) {
792  $selected_word_key = array_search($correct_position, $selected_word_positions);
793  if ($selected_word_key === false) {
794  continue;
795  }
796 
797  if ($correct_position_data['length'] === 1) {
798  $points += $correct_position_data['points'];
799  unset($selected_word_positions[$selected_word_key]);
800  continue;
801  }
802 
803  $passage_complete = true;
804  for ($i = 1;$i < $correct_position_data['length'];$i++) {
805  $selected_passage_element_key = array_search($correct_position + $i, $selected_word_positions);
806  if ($selected_passage_element_key === false) {
807  $passage_complete = false;
808  continue;
809  }
810  unset($selected_word_positions[$selected_passage_element_key]);
811  }
812 
813  if ($passage_complete) {
814  $points += $correct_position_data['points'];
815  unset($selected_word_positions[$selected_word_key]);
816  }
817  }
818 
819  foreach ($selected_word_positions as $word_position) {
820  if (!array_key_exists($word_position, $correct_positions)) {
821  $points += $this->getPointsWrong();
822  continue;
823  }
824  }
825 
826  return $points;
827  }
828 
829  public function flushErrorData(): void
830  {
831  $this->errordata = [];
832  }
833 
838  public function getErrorData(): array
839  {
840  return $this->errordata;
841  }
842 
847  private function getErrorDataAsArrayForJS(): array
848  {
849  $correct_answers = [];
850  foreach ($this->getErrorData() as $index => $answer_obj) {
851  $correct_answers[] = [
852  'answertext_wrong' => $answer_obj->getTextWrong(),
853  'answertext_correct' => $answer_obj->getTextCorrect(),
854  'points' => $answer_obj->getPoints(),
855  'length' => $answer_obj->getLength(),
856  'pos' => $this->getId() . '_' . $answer_obj->getPosition()
857  ];
858  }
859  return $correct_answers;
860  }
861 
862  public function getErrorText(): string
863  {
864  return $this->errortext ?? '';
865  }
866 
867  public function setErrorText(?string $text): void
868  {
869  $this->errortext = $text ?? '';
870  }
871 
872  public function getParsedErrorText(): array
873  {
875  }
876 
877  private function getParsedErrorTextForJS(): array
878  {
879  $answers = [];
880  foreach ($this->parsed_errortext as $paragraph) {
881  foreach ($paragraph as $position => $word) {
882  $answers[] = [
883  'answertext' => $word['text'],
884  'order' => $this->getId() . '_' . $position
885  ];
886  }
887  $answers[] = [
888  'answertext' => '###'
889  ];
890  }
891  array_pop($answers);
892 
893  return $answers;
894  }
895 
896  public function setParsedErrorText(array $parsed_errortext): void
897  {
898  $this->parsed_errortext = $parsed_errortext;
899  }
900 
901  public function getTextSize(): float
902  {
903  return $this->textsize;
904  }
905 
906  public function setTextSize($a_value): void
907  {
908  // in self-assesment-mode value should always be set (and must not be null)
909  if ($a_value === null) {
910  $a_value = 100;
911  }
912  $this->textsize = $a_value;
913  }
914 
915  public function getPointsWrong(): ?float
916  {
917  return $this->points_wrong;
918  }
919 
920  public function setPointsWrong($a_value): void
921  {
922  $this->points_wrong = $a_value;
923  }
924 
925  public function toJSON(): string
926  {
927  $result = [];
928  $result['id'] = $this->getId();
929  $result['type'] = (string) $this->getQuestionType();
930  $result['title'] = $this->getTitleForHTMLOutput();
931  $result['question'] = $this->formatSAQuestion($this->getQuestion());
932  $result['text'] = ilRTE::_replaceMediaObjectImageSrc($this->getErrorText(), 0);
933  $result['nr_of_tries'] = $this->getNrOfTries();
934  $result['shuffle'] = $this->getShuffle();
935  $result['feedback'] = [
936  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
937  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
938  ];
939 
940  $result['correct_answers'] = $this->getErrorDataAsArrayForJS();
941  $result['answers'] = $this->getParsedErrorTextForJS();
942 
943  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
944  $result['mobs'] = $mobs;
945 
946  return json_encode($result);
947  }
948 
957  public function getOperators($expression): array
958  {
960  }
961 
966  public function getExpressionTypes(): array
967  {
968  return [
973  ];
974  }
975 
982  public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
983  {
984  $result = new ilUserQuestionResult($this, $active_id, $pass);
985 
986  $data = $this->db->queryF(
987  "SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = (
988  SELECT MAX(step) FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s
989  )",
990  ["integer", "integer", "integer","integer", "integer", "integer"],
991  [$active_id, $pass, $this->getId(), $active_id, $pass, $this->getId()]
992  );
993 
994  while ($row = $this->db->fetchAssoc($data)) {
995  $result->addKeyValue($row["value1"], $row["value1"]);
996  }
997 
998  $points = $this->calculateReachedPoints($active_id, $pass);
999  $max_points = $this->getMaximumPoints();
1000 
1001  $result->setReachedPercentage(($points / $max_points) * 100);
1002 
1003  return $result;
1004  }
1005 
1006  public function parseErrorText(): void
1007  {
1008  $text_by_paragraphs = preg_split(self::PARAGRAPH_SPLIT_REGEXP, $this->getErrorText());
1009  $text_array = [];
1010  $offset = 0;
1011  foreach ($text_by_paragraphs as $paragraph) {
1012  $text_array[] = $this->addErrorInformationToTextParagraphArray(
1013  preg_split(self::WORD_SPLIT_REGEXP, trim($paragraph)),
1014  $offset
1015  );
1016  $offset += count(end($text_array));
1017  }
1018  $this->setParsedErrorText($text_array);
1019  }
1020 
1026  private function addErrorInformationToTextParagraphArray(array $paragraph, int $offset): array
1027  {
1028  $paragraph_with_error_info = [];
1029  $passage_start = null;
1030  foreach ($paragraph as $position => $word) {
1031  $actual_position = $position + $offset;
1032  if ($passage_start !== null
1033  && (mb_strrpos($word, self::ERROR_PARAGRAPH_DELIMITERS['end']) === mb_strlen($word) - 2
1034  || mb_strrpos($word, self::ERROR_PARAGRAPH_DELIMITERS['end']) === mb_strlen($word) - 3
1035  && preg_match(self::FIND_PUNCTUATION_REGEXP, mb_substr($word, -1)) === 1)) {
1036  $actual_word = $this->parsePassageEndWord($word);
1037 
1038  $paragraph_with_error_info[$passage_start]['text_wrong'] .=
1039  ' ' . $actual_word;
1040  $paragraph_with_error_info[$actual_position] = [
1041  'text' => $actual_word,
1042  'error_type' => 'passage_end'
1043  ];
1044  $passage_start = null;
1045  continue;
1046  }
1047  if ($passage_start !== null) {
1048  $paragraph_with_error_info[$passage_start]['text_wrong'] .= ' ' . $word;
1049  $paragraph_with_error_info[$actual_position] = [
1050  'text' => $word,
1051  'error_type' => 'in_passage'
1052  ];
1053  continue;
1054  }
1055  if (mb_strpos($word, self::ERROR_PARAGRAPH_DELIMITERS['start']) === 0) {
1056  $paragraph_with_error_info[$actual_position] = [
1057  'text' => substr($word, 2),
1058  'text_wrong' => substr($word, 2),
1059  'error_type' => 'passage_start',
1060  'error_position' => $actual_position,
1061  ];
1062  $passage_start = $actual_position;
1063  continue;
1064  }
1065  if (mb_strpos($word, self::ERROR_WORD_MARKER) === 0) {
1066  $paragraph_with_error_info[$actual_position] = [
1067  'text' => substr($word, 1),
1068  'text_wrong' => substr($word, 1),
1069  'error_type' => 'word',
1070  'error_position' => $actual_position,
1071  ];
1072  continue;
1073  }
1074 
1075  $paragraph_with_error_info[$actual_position] = [
1076  'text' => $word,
1077  'error_type' => 'none',
1078  'points' => $this->getPointsWrong()
1079  ];
1080  }
1081 
1082  return $paragraph_with_error_info;
1083  }
1084 
1085  private function parsePassageEndWord(string $word): string
1086  {
1087  if (mb_substr($word, -2) === self::ERROR_PARAGRAPH_DELIMITERS['end']) {
1088  return mb_substr($word, 0, -2);
1089  }
1090  return mb_substr($word, 0, -3) . mb_substr($word, -1);
1091  }
1092 
1100  public function getAvailableAnswerOptions($index = null): ?int
1101  {
1102  $error_text_array = array_reduce(
1103  $this->parsed_errortext,
1104  fn($c, $v) => $c + $v
1105  );
1106 
1107  if ($index === null) {
1108  return $error_text_array;
1109  }
1110 
1111  if (array_key_exists($index, $error_text_array)) {
1112  return $error_text_array[$index];
1113  }
1114 
1115  return null;
1116  }
1117 
1118  private function generateArrayByPositionFromErrorData(): array
1119  {
1120  $array_by_position = [];
1121  foreach ($this->errordata as $error) {
1122  $array_by_position[$error->getPosition()] = [
1123  'length' => $error->getLength(),
1124  'points' => $error->getPoints(),
1125  'text' => $error->getTextWrong(),
1126  'text_correct' => $error->getTextCorrect()
1127  ];
1128  }
1129  ksort($array_by_position);
1130  return $array_by_position;
1131  }
1132 
1138  private function getErrorTokenHtml($item, $class, $useLinkTags): string
1139  {
1140  if ($useLinkTags) {
1141  return '<a class="' . $class . '" href="#">' . ($item == '&nbsp;' ? $item : ilLegacyFormElementsUtil::prepareFormOutput(
1142  $item
1143  )) . '</a>';
1144  }
1145 
1146  return '<span class="' . $class . '">' . ($item == '&nbsp;' ? $item : ilLegacyFormElementsUtil::prepareFormOutput(
1147  $item
1148  )) . '</span>';
1149  }
1150 }
getPointsForSelectedPositions(array $selected_word_positions)
static _replaceMediaObjectImageSrc(string $a_text, int $a_direction=0, string $nic='')
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
const FIND_PUNCTUATION_REGEXP
setNrOfTries(int $a_nr_of_tries)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Abstract basic class which is to be extended by the concrete assessment question type classes...
loadFromDb($question_id)
Loads the object from the database.
setOwner(int $owner=-1)
addErrorInformationToTextParagraphArray(array $paragraph, int $offset)
toXML($a_include_header=true, $a_include_binary=true, $a_shuffle=false, $test_output=false, $force_image_references=false)
saveWorkingData($active_id, $pass=null, $authorized=true)
Saves the learners input of the question to the database.
ensureNonNegativePoints($points)
saveToDb($original_id="")
Saves a the object to the database.
getClassForPosition(int $position, bool $show_correct_solution, array $selections)
getTextForPosition(int $position, array $paragraph, bool $show_correct_solution)
copyObject($target_questionpool_id, $title="")
Copies an object.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setCell($a_row, $a_col, $a_value, $datatype=null)
static prepareFormOutput($a_str, bool $a_strip=false)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setComment(string $comment="")
__construct( $title='', $comment='', $author='', $owner=-1, $question='')
assErorText constructor
float $points
The maximum available points for the question.
const ERROR_PARAGRAPH_DELIMITERS
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
getUserQuestionResult($active_id, $pass)
Get the user solution for a question by active_id and the test pass.
Base Exception for all Exceptions relating to Modules/Test.
getCorrectnessIconForPosition(int $position, bool $graphical_output, array $selections, array $correctness_icons)
duplicate(bool $for_test=true, string $title="", string $author="", int $owner=-1, $testObjId=null)
Duplicates the object.
setErrorData(array $errors)
isComplete()
Returns true, if a single choice question is complete for use.
createErrorTextExport(array $selections)
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
__construct(VocabulariesInterface $vocabularies)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
parsePassageEndWord(string $word)
logUserAction(bool $user_entered_values, int $active_id)
const string $errortext
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
assembleErrorTextOutput(array $selections, bool $graphical_output=false, bool $show_correct_solution=false, bool $use_link_tags=true, array $correctness_icons=[])
fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints=[])
static logAction(string $logtext, int $active_id, int $question_id)
const PARAGRAPH_SPLIT_REGEXP
setPointsWrong($a_value)
getOperators($expression)
Get all available operations for a specific question.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
string $key
Consumer key/client ID value.
Definition: System.php:193
getExpressionTypes()
Get all available expression types for a specific question.
setErrorText(?string $text)
addPositionToErrorAnswer(assAnswerErrorText $error)
savePreviewData(ilAssQuestionPreviewSession $previewSession)
setPoints(float $points)
setObjId(int $obj_id=0)
string $question
The question text.
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
generateOutputStringFromPosition(int $position, array $selections, array $paragraph, bool $graphical_output, bool $show_correct_solution, bool $use_link_tags, array $correctness_icons)
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
saveQuestionDataToDb(int $original_id=-1)
getSolutionMaxPass(int $active_id)
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setId(int $id=-1)
getBestSelection($withPositivePointsOnly=true)
setOriginalId(?int $original_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setTitle(string $title="")
setLifecycle(ilAssQuestionLifecycle $lifecycle)
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
saveAdditionalQuestionDataToDb()
Saves the data for the additional data table.
completeParsedErrorTextFromErrorData()
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
setAuthor(string $author="")
setParsedErrorText(array $parsed_errortext)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
getAdditionalInformationFromExistingErrorDataByErrorText(array $current_error_data, string $text_wrong)
setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass)
{}
getErrorTokenHtml($item, $class, $useLinkTags)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setQuestion(string $question="")