ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
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'], 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  if ($db_error_text->numRows() > 0) {
211  while ($data = $this->db->fetchAssoc($db_error_text)) {
212  $this->errordata[] = new assAnswerErrorText(
213  (string) $data['text_wrong'],
214  (string) $data['text_correct'],
215  (float) $data['points'],
216  $data['position']
217  );
218  }
219  }
220 
222 
223  parent::loadFromDb($question_id);
224  }
225 
226  private function correctDataAfterParserUpdate(): void
227  {
228  if ($this->getErrorText() === '') {
229  return;
230  }
231  $needs_finalizing = false;
232  if ($this->getParsedErrorText() === []) {
233  $needs_finalizing = true;
234  $this->parseErrorText();
235  }
236 
237  if (isset($this->errordata[0])
238  && $this->errordata[0]->getPosition() === null) {
239  foreach ($this->errordata as $key => $error) {
240  $this->errordata[$key] = $this->addPositionToErrorAnswer($error);
241  }
243  }
244 
245  if ($needs_finalizing) {
248  }
249  }
250 
254  public function duplicate(bool $for_test = true, string $title = "", string $author = "", string $owner = "", $testObjId = null): int
255  {
256  if ($this->id <= 0) {
257  // The question has not been saved. It cannot be duplicated
258  return -1;
259  }
260  // duplicate the question in database
261  $this_id = $this->getId();
262  $thisObjId = $this->getObjId();
263 
264  $clone = $this;
266  $clone->id = -1;
267 
268  if ((int) $testObjId > 0) {
269  $clone->setObjId($testObjId);
270  }
271 
272  if ($title) {
273  $clone->setTitle($title);
274  }
275 
276  if ($author) {
277  $clone->setAuthor($author);
278  }
279  if ($owner) {
280  $clone->setOwner($owner);
281  }
282 
283  if ($for_test) {
284  $clone->saveToDb($original_id);
285  } else {
286  $clone->saveToDb();
287  }
288  // copy question page content
289  $clone->copyPageOfQuestion($this_id);
290  // copy XHTML media objects
291  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
292 
293  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
294  return $clone->id;
295  }
296 
300  public function copyObject($target_questionpool_id, $title = ""): int
301  {
302  if ($this->getId() <= 0) {
303  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
304  }
305  // duplicate the question in database
306 
307  $thisId = $this->getId();
308  $thisObjId = $this->getObjId();
309 
310  $clone = $this;
312  $clone->id = -1;
313  $clone->setObjId($target_questionpool_id);
314  if ($title) {
315  $clone->setTitle($title);
316  }
317  $clone->saveToDb();
318 
319  // copy question page content
320  $clone->copyPageOfQuestion($original_id);
321  // copy XHTML media objects
322  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
323 
324  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
325 
326  return $clone->id;
327  }
328 
329  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
330  {
331  if ($this->getId() <= 0) {
332  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
333  }
334 
335  $sourceQuestionId = $this->id;
336  $sourceParentId = $this->getObjId();
337 
338  // duplicate the question in database
339  $clone = $this;
340  $clone->id = -1;
341 
342  $clone->setObjId($targetParentId);
343 
344  if ($targetQuestionTitle) {
345  $clone->setTitle($targetQuestionTitle);
346  }
347 
348  $clone->saveToDb();
349  // copy question page content
350  $clone->copyPageOfQuestion($sourceQuestionId);
351  // copy XHTML media objects
352  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
353 
354  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
355 
356  return $clone->id;
357  }
358 
364  public function getMaximumPoints(): float
365  {
366  $maxpoints = 0.0;
367  foreach ($this->errordata as $error) {
368  if ($error->getPoints() > 0) {
369  $maxpoints += $error->getPoints();
370  }
371  }
372  return $maxpoints;
373  }
374 
385  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float
386  {
387  if ($returndetails) {
388  throw new ilTestException('return details not implemented for ' . __METHOD__);
389  }
390 
391  /* First get the positions which were selected by the user. */
392  $positions = [];
393  if (is_null($pass)) {
394  $pass = $this->getSolutionMaxPass($active_id);
395  }
396  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
397 
398  while ($row = $this->db->fetchAssoc($result)) {
399  $positions[] = $row['value1'];
400  }
401  $points = $this->getPointsForSelectedPositions($positions);
402  return $points;
403  }
404 
406  {
407  $reached_points = $this->getPointsForSelectedPositions($preview_session->getParticipantsSolution() ?? []);
408  $reached_points = $this->deductHintPointsFromReachedPoints($preview_session, $reached_points);
409  return $this->ensureNonNegativePoints($reached_points);
410  }
411 
420  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
421  {
422  if (is_null($pass)) {
423  $pass = ilObjTest::_getPass($active_id);
424  }
425 
426  $selected = $this->getAnswersFromRequest();
427  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
428  function () use ($selected, $active_id, $pass, $authorized) {
429  $this->removeCurrentSolution($active_id, $pass, $authorized);
430 
431  foreach ($selected as $position) {
432  $this->saveCurrentSolution($active_id, $pass, $position, null, $authorized);
433  }
434  }
435  );
436 
438  $this->logUserAction($selected !== [], (int) $active_id);
439  }
440 
441  return true;
442  }
443 
444  public function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
445  {
446  $selection = $this->getAnswersFromRequest();
447  $previewSession->setParticipantsSolution($selection);
448  }
449 
450  private function logUserAction(bool $user_entered_values, int $active_id): void
451  {
452  $log_text = $this->lng->txtlng(
453  "assessment",
454  $user_entered_values ? 'log_user_entered_values' : 'log_user_not_entered_values',
456  );
457  assQuestion::logAction($log_text, $active_id, $this->getId());
458  }
459 
460  private function getAnswersFromRequest(): array
461  {
462  if (mb_strlen($_POST["qst_" . $this->getId()])) {
463  return explode(',', $_POST["qst_{$this->getId()}"]);
464  }
465 
466  return [];
467  }
468 
469  public function getQuestionType(): string
470  {
471  return 'assErrorText';
472  }
473 
474  public function getAdditionalTableName(): string
475  {
476  return 'qpl_qst_errortext';
477  }
478 
479  public function getAnswerTableName(): string
480  {
481  return 'qpl_a_errortext';
482  }
483 
487  public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass): int
488  {
489  parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
490 
491  $i = 0;
492  $selections = [];
493  $solutions = $this->getSolutionValues($active_id, $pass);
494  if (is_array($solutions)) {
495  foreach ($solutions as $solution) {
496  $selections[] = $solution['value1'];
497  }
498  }
499  $errortext = $this->createErrorTextExport($selections);
500  $i++;
501  $worksheet->setCell($startrow + $i, 2, $errortext);
502  $i++;
503 
504  return $startrow + $i + 1;
505  }
506 
507  public function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array
508  {
509  $import = new assErrorTextImport($this);
510  return $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping);
511  }
512 
513  public function toXML($a_include_header = true, $a_include_binary = true, $a_shuffle = false, $test_output = false, $force_image_references = false): string
514  {
515  $export = new assErrorTextExport($this);
516  return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
517  }
518 
519  public function setErrorsFromParsedErrorText(): void
520  {
521  $current_error_data = $this->getErrorData();
522  $this->errordata = [];
523 
524  $has_too_long_errors = false;
525  foreach ($this->getParsedErrorText() as $paragraph) {
526  foreach ($paragraph as $position => $word) {
527  if ($word['error_type'] === 'in_passage'
528  || $word['error_type'] === 'passage_end'
529  || $word['error_type'] === 'none') {
530  continue;
531  }
532 
533  $text_wrong = $word['text_wrong'];
534  if (mb_strlen($text_wrong) > self::ERROR_MAX_LENGTH) {
535  $has_too_long_errors = true;
536  continue;
537  }
538 
539  list($text_correct, $points) =
540  $this->getAdditionalInformationFromExistingErrorDataByErrorText($current_error_data, $text_wrong);
541  $this->errordata[] = new assAnswerErrorText($text_wrong, $text_correct, $points, $position);
542  }
543  }
544 
545  if ($has_too_long_errors) {
546  $this->tpl->setOnScreenMessage(
547  'failure',
548  $this->lng->txt('qst_error_text_too_long')
549  );
550  }
551  }
552 
554  {
555  foreach ($this->getParsedErrorText() as $paragraph) {
556  foreach ($paragraph as $position => $word) {
557  if (isset($word['text_wrong'])
558  && ($word['text_wrong'] === $error->getTextWrong()
559  || mb_substr($word['text_wrong'], 0, -1) === $error->getTextWrong()
560  && preg_match(self::FIND_PUNCTUATION_REGEXP, mb_substr($word['text_wrong'], -1)) === 1)
561  && !array_key_exists($position, $this->generateArrayByPositionFromErrorData())
562  ) {
563  return $error->withPosition($position);
564  }
565 
566  }
567  }
568 
569  return $error;
570  }
571 
572  private function completeParsedErrorTextFromErrorData(): void
573  {
574  foreach ($this->errordata as $error) {
575  $position = $error->getPosition();
576  foreach ($this->getParsedErrorText() as $key => $paragraph) {
577  if (array_key_exists($position, $paragraph)) {
578  $this->parsed_errortext[$key][$position]['text_correct'] =
579  $error->getTextCorrect();
580  $this->parsed_errortext[$key][$position]['points'] =
581  $error->getPoints();
582  break;
583  }
584  }
585  }
586  }
587 
592  public function setErrorData(array $errors): void
593  {
594  $this->errordata = [];
595 
596  foreach ($errors as $error) {
597  $answer = $this->addPositionToErrorAnswer($error);
598  $this->errordata[] = $answer;
599  }
601  }
602 
603  public function removeErrorDataWithoutPosition(): void
604  {
605  foreach ($this->getErrorData() as $index => $error) {
606  if ($error->getPosition() === null) {
607  unset($this->errordata[$index]);
608  }
609  }
610  $this->errordata = array_values($this->errordata);
611  }
612 
619  array $current_error_data,
620  string $text_wrong
621  ): array {
622  foreach ($current_error_data as $answer_object) {
623  if (strcmp($answer_object->getTextWrong(), $text_wrong) === 0) {
624  return[
625  $answer_object->getTextCorrect(),
626  $answer_object->getPoints()
627  ];
628  }
629  }
630  return ['', 0.0];
631  }
632 
633  public function assembleErrorTextOutput(
634  array $selections,
635  bool $graphical_output = false,
636  bool $show_correct_solution = false,
637  bool $use_link_tags = true,
638  array $correctness_icons = []
639  ): string {
640  $output_array = [];
641  foreach ($this->getParsedErrorText() as $paragraph) {
642  $array_reduce_function = fn (?string $carry, int $position)
643  => $carry . $this->generateOutputStringFromPosition(
644  $position,
645  $selections,
646  $paragraph,
647  $graphical_output,
648  $show_correct_solution,
649  $use_link_tags,
650  $correctness_icons
651  );
652  $output_array[] = '<p>' . trim(array_reduce(array_keys($paragraph), $array_reduce_function)) . '</p>';
653  }
654 
655  return implode("\n", $output_array);
656  }
657 
659  int $position,
660  array $selections,
661  array $paragraph,
662  bool $graphical_output,
663  bool $show_correct_solution,
664  bool $use_link_tags,
665  array $correctness_icons
666  ): string {
667  $text = $this->getTextForPosition($position, $paragraph, $show_correct_solution);
668  if ($text === '') {
669  return '';
670  }
671  $class = $this->getClassForPosition($position, $show_correct_solution, $selections);
673  $position,
674  $graphical_output,
675  $selections,
676  $correctness_icons
677  );
678 
679  return ' ' . $this->getErrorTokenHtml($text, $class, $use_link_tags) . $img;
680  }
681 
682  private function getTextForPosition(
683  int $position,
684  array $paragraph,
685  bool $show_correct_solution
686  ): string {
687  $v = $paragraph[$position];
688  if ($show_correct_solution === true
689  && ($v['error_type'] === 'in_passage'
690  || $v['error_type'] === 'passage_end')) {
691  return '';
692  }
693  if ($show_correct_solution
694  && ($v['error_type'] === 'passage_start'
695  || $v['error_type'] === 'word')) {
696  return $v['text_correct'] ?? '';
697  }
698 
699  return $v['text'];
700  }
701 
702  private function getClassForPosition(
703  int $position,
704  bool $show_correct_solution,
705  array $selections
706  ): string {
707  if ($show_correct_solution !== true
708  && in_array($position, $selections['user'])) {
709  return 'ilc_qetitem_ErrorTextSelected';
710  }
711 
712  if ($show_correct_solution === true
713  && in_array($position, $selections['best'])) {
714  return 'ilc_qetitem_ErrorTextSelected';
715  }
716 
717  return 'ilc_qetitem_ErrorTextItem';
718  }
719 
721  int $position,
722  bool $graphical_output,
723  array $selections,
724  array $correctness_icons
725  ): string {
726  if ($graphical_output === true
727  && (in_array($position, $selections['user']) && !in_array($position, $selections['best'])
728  || !in_array($position, $selections['user']) && in_array($position, $selections['best']))) {
729  return $correctness_icons['not_correct'];
730  }
731 
732  if ($graphical_output === true
733  && in_array($position, $selections['user']) && in_array($position, $selections['best'])) {
734  return $correctness_icons['correct'];
735  }
736 
737  return '';
738  }
739 
740  public function createErrorTextExport(array $selections): string
741  {
742  if (!is_array($selections)) {
743  $selections = [];
744  }
745 
746  foreach ($this->getParsedErrorText() as $paragraph) {
747  $array_reduce_function = function ($carry, $k) use ($paragraph, $selections) {
748  $text = $paragraph[$k]['text'];
749  if (in_array($k, $selections)) {
750  $text = self::ERROR_WORD_MARKER . $paragraph[$k]['text'] . self::ERROR_WORD_MARKER;
751  }
752  return $carry . ' ' . $text;
753  };
754  $output_array[] = trim(array_reduce(array_keys($paragraph), $array_reduce_function));
755  }
756  return implode("\n", $output_array);
757  }
758 
759  public function getBestSelection($withPositivePointsOnly = true): array
760  {
761  $positions_array = $this->generateArrayByPositionFromErrorData();
762  $selections = [];
763  foreach ($positions_array as $position => $position_data) {
764  if ($position === ''
765  || $withPositivePointsOnly && $position_data['points'] < 1) {
766  continue;
767  }
768 
769  $selections[] = $position;
770  if ($position_data['length'] > 1) {
771  for ($i = 1;$i < $position_data['length'];$i++) {
772  $selections[] = $position + $i;
773  }
774  }
775  }
776 
777  return $selections;
778  }
779 
784  protected function getPointsForSelectedPositions(array $selected_word_positions): float
785  {
786  $points = 0;
787  $correct_positions = $this->generateArrayByPositionFromErrorData();
788 
789  foreach ($correct_positions as $correct_position => $correct_position_data) {
790  $selected_word_key = array_search($correct_position, $selected_word_positions);
791  if ($selected_word_key === false) {
792  continue;
793  }
794 
795  if ($correct_position_data['length'] === 1) {
796  $points += $correct_position_data['points'];
797  unset($selected_word_positions[$selected_word_key]);
798  continue;
799  }
800 
801  $passage_complete = true;
802  for ($i = 1;$i < $correct_position_data['length'];$i++) {
803  $selected_passage_element_key = array_search($correct_position + $i, $selected_word_positions);
804  if ($selected_passage_element_key === false) {
805  $passage_complete = false;
806  continue;
807  }
808  unset($selected_word_positions[$selected_passage_element_key]);
809  }
810 
811  if ($passage_complete) {
812  $points += $correct_position_data['points'];
813  unset($selected_word_positions[$selected_word_key]);
814  }
815  }
816 
817  foreach ($selected_word_positions as $word_position) {
818  if (!array_key_exists($word_position, $correct_positions)) {
819  $points += $this->getPointsWrong();
820  continue;
821  }
822  }
823 
824  return $points;
825  }
826 
827  public function flushErrorData(): void
828  {
829  $this->errordata = [];
830  }
831 
836  public function getErrorData(): array
837  {
838  return $this->errordata;
839  }
840 
845  private function getErrorDataAsArrayForJS(): array
846  {
847  $correct_answers = [];
848  foreach ($this->getErrorData() as $index => $answer_obj) {
849  $correct_answers[] = [
850  'answertext_wrong' => $answer_obj->getTextWrong(),
851  'answertext_correct' => $answer_obj->getTextCorrect(),
852  'points' => $answer_obj->getPoints(),
853  'length' => $answer_obj->getLength(),
854  'pos' => $this->getId() . '_' . $answer_obj->getPosition()
855  ];
856  }
857  return $correct_answers;
858  }
859 
860  public function getErrorText(): string
861  {
862  return $this->errortext ?? '';
863  }
864 
865  public function setErrorText(?string $text): void
866  {
867  $this->errortext = $text ?? '';
868  }
869 
870  public function getParsedErrorText(): array
871  {
873  }
874 
875  private function getParsedErrorTextForJS(): array
876  {
877  $answers = [];
878  foreach ($this->parsed_errortext as $paragraph) {
879  foreach ($paragraph as $position => $word) {
880  $answers[] = [
881  'answertext' => $word['text'],
882  'order' => $this->getId() . '_' . $position
883  ];
884  }
885  $answers[] = [
886  'answertext' => '###'
887  ];
888  }
889  array_pop($answers);
890 
891  return $answers;
892  }
893 
894  public function setParsedErrorText(array $parsed_errortext): void
895  {
896  $this->parsed_errortext = $parsed_errortext;
897  }
898 
899  public function getTextSize(): float
900  {
901  return $this->textsize;
902  }
903 
904  public function setTextSize($a_value): void
905  {
906  // in self-assesment-mode value should always be set (and must not be null)
907  if ($a_value === null) {
908  $a_value = 100;
909  }
910  $this->textsize = $a_value;
911  }
912 
913  public function getPointsWrong(): ?float
914  {
915  return $this->points_wrong;
916  }
917 
918  public function setPointsWrong($a_value): void
919  {
920  $this->points_wrong = $a_value;
921  }
922 
923  public function toJSON(): string
924  {
925  $result = [];
926  $result['id'] = $this->getId();
927  $result['type'] = (string) $this->getQuestionType();
928  $result['title'] = $this->getTitleForHTMLOutput();
929  $result['question'] = $this->formatSAQuestion($this->getQuestion());
930  $result['text'] = ilRTE::_replaceMediaObjectImageSrc($this->getErrorText(), 0);
931  $result['nr_of_tries'] = $this->getNrOfTries();
932  $result['shuffle'] = $this->getShuffle();
933  $result['feedback'] = [
934  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
935  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
936  ];
937 
938  $result['correct_answers'] = $this->getErrorDataAsArrayForJS();
939  $result['answers'] = $this->getParsedErrorTextForJS();
940 
941  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
942  $result['mobs'] = $mobs;
943 
944  return json_encode($result);
945  }
946 
955  public function getOperators($expression): array
956  {
958  }
959 
964  public function getExpressionTypes(): array
965  {
966  return [
971  ];
972  }
973 
980  public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
981  {
982  $result = new ilUserQuestionResult($this, $active_id, $pass);
983 
984  $data = $this->db->queryF(
985  "SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = (
986  SELECT MAX(step) FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s
987  )",
988  ["integer", "integer", "integer","integer", "integer", "integer"],
989  [$active_id, $pass, $this->getId(), $active_id, $pass, $this->getId()]
990  );
991 
992  while ($row = $this->db->fetchAssoc($data)) {
993  $result->addKeyValue($row["value1"], $row["value1"]);
994  }
995 
996  $points = $this->calculateReachedPoints($active_id, $pass);
997  $max_points = $this->getMaximumPoints();
998 
999  $result->setReachedPercentage(($points / $max_points) * 100);
1000 
1001  return $result;
1002  }
1003 
1004  public function parseErrorText(): void
1005  {
1006  $text_by_paragraphs = preg_split(self::PARAGRAPH_SPLIT_REGEXP, $this->getErrorText());
1007  $text_array = [];
1008  $offset = 0;
1009  foreach ($text_by_paragraphs as $paragraph) {
1010  $text_array[] = $this->addErrorInformationToTextParagraphArray(
1011  preg_split(self::WORD_SPLIT_REGEXP, trim($paragraph)),
1012  $offset
1013  );
1014  $offset += count(end($text_array));
1015  }
1016  $this->setParsedErrorText($text_array);
1017  }
1018 
1024  private function addErrorInformationToTextParagraphArray(array $paragraph, int $offset): array
1025  {
1026  $paragraph_with_error_info = [];
1027  $passage_start = null;
1028  foreach ($paragraph as $position => $word) {
1029  $actual_position = $position + $offset;
1030  if ($passage_start !== null
1031  && (mb_strrpos($word, self::ERROR_PARAGRAPH_DELIMITERS['end']) === mb_strlen($word) - 2
1032  || mb_strrpos($word, self::ERROR_PARAGRAPH_DELIMITERS['end']) === mb_strlen($word) - 3
1033  && preg_match(self::FIND_PUNCTUATION_REGEXP, mb_substr($word, -1)) === 1)) {
1034 
1035  $actual_word = $this->parsePassageEndWord($word);
1036 
1037  $paragraph_with_error_info[$passage_start]['text_wrong'] .=
1038  ' ' . $actual_word;
1039  $paragraph_with_error_info[$actual_position] = [
1040  'text' => $actual_word,
1041  'error_type' => 'passage_end'
1042  ];
1043  $passage_start = null;
1044  continue;
1045  }
1046  if ($passage_start !== null) {
1047  $paragraph_with_error_info[$passage_start]['text_wrong'] .= ' ' . $word;
1048  $paragraph_with_error_info[$actual_position] = [
1049  'text' => $word,
1050  'error_type' => 'in_passage'
1051  ];
1052  continue;
1053  }
1054  if (mb_strpos($word, self::ERROR_PARAGRAPH_DELIMITERS['start']) === 0) {
1055  $paragraph_with_error_info[$actual_position] = [
1056  'text' => substr($word, 2),
1057  'text_wrong' => substr($word, 2),
1058  'error_type' => 'passage_start',
1059  'error_position' => $actual_position,
1060  ];
1061  $passage_start = $actual_position;
1062  continue;
1063  }
1064  if (mb_strpos($word, self::ERROR_WORD_MARKER) === 0) {
1065  $paragraph_with_error_info[$actual_position] = [
1066  'text' => substr($word, 1),
1067  'text_wrong' => substr($word, 1),
1068  'error_type' => 'word',
1069  'error_position' => $actual_position,
1070  ];
1071  continue;
1072  }
1073 
1074  $paragraph_with_error_info[$actual_position] = [
1075  'text' => $word,
1076  'error_type' => 'none',
1077  'points' => $this->getPointsWrong()
1078  ];
1079  }
1080 
1081  return $paragraph_with_error_info;
1082  }
1083 
1084  private function parsePassageEndWord(string $word): string
1085  {
1086  if (mb_substr($word, -2) === self::ERROR_PARAGRAPH_DELIMITERS['end']) {
1087  return mb_substr($word, 0, -2);
1088  }
1089  return mb_substr($word, 0, -3) . mb_substr($word, -1);
1090  }
1091 
1099  public function getAvailableAnswerOptions($index = null): ?int
1100  {
1101  $error_text_array = array_reduce(
1102  $this->parsed_errortext,
1103  fn ($c, $v) => $c + $v
1104  );
1105 
1106  if ($index === null) {
1107  return $error_text_array;
1108  }
1109 
1110  if (array_key_exists($index, $error_text_array)) {
1111  return $error_text_array[$index];
1112  }
1113 
1114  return null;
1115  }
1116 
1117  private function generateArrayByPositionFromErrorData(): array
1118  {
1119  $array_by_position = [];
1120  foreach ($this->errordata as $error) {
1121  $array_by_position[$error->getPosition()] = [
1122  'length' => $error->getLength(),
1123  'points' => $error->getPoints(),
1124  'text' => $error->getTextWrong(),
1125  'text_correct' => $error->getTextCorrect()
1126  ];
1127  }
1128  ksort($array_by_position);
1129  return $array_by_position;
1130  }
1131 
1137  private function getErrorTokenHtml($item, $class, $useLinkTags): string
1138  {
1139  if ($useLinkTags) {
1140  return '<a class="' . $class . '" href="#">' . ($item == '&nbsp;' ? $item : ilLegacyFormElementsUtil::prepareFormOutput(
1141  $item
1142  )) . '</a>';
1143  }
1144 
1145  return '<span class="' . $class . '">' . ($item == '&nbsp;' ? $item : ilLegacyFormElementsUtil::prepareFormOutput(
1146  $item
1147  )) . '</span>';
1148  }
1149 }
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.
$c
Definition: cli.php:38
$mobs
Definition: imgupload.php:70
$errors
Definition: imgupload.php:65
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)
static _getOriginalId(int $question_id)
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.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$index
Definition: metadata.php:145
getCorrectnessIconForPosition(int $position, bool $graphical_output, array $selections, array $correctness_icons)
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $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)
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.
$img
Definition: imgupload.php:83
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
{}
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)
__construct(Container $dic, ilPlugin $plugin)
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)
getErrorTokenHtml($item, $class, $useLinkTags)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$i
Definition: metadata.php:41
setQuestion(string $question="")