ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.assClozeTest.php
Go to the documentation of this file.
1 <?php
2 
19 use ILIAS\Refinery\Random\Group as RandomGroup;
20 
21 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
22 
35 {
40  public array $gaps = [];
41 
50 
51 
53 
61  public $start_tag;
62 
70  public $end_tag;
71 
83 
94 
101 
102  public $cloze_text;
103 
108 
110 
111  private RandomGroup $randomGroup;
112 
124  public function __construct(
125  $title = "",
126  $comment = "",
127  $author = "",
128  $owner = -1,
129  $question = ""
130  ) {
131  global $DIC;
132 
134  $this->start_tag = "[gap]";
135  $this->end_tag = "[/gap]";
136  $this->gaps = [];
137  $this->setQuestion($question); // @TODO: Should this be $question?? See setter for why this is not trivial.
138  $this->fixedTextLength = "";
139  $this->identical_scoring = 1;
140  $this->gap_combinations_exists = false;
141  $this->gap_combinations = [];
142  $this->randomGroup = $DIC->refinery()->random();
143  }
144 
150  public function isComplete(): bool
151  {
152  if (strlen($this->getTitle())
153  && $this->getAuthor()
154  && $this->getClozeText()
155  && count($this->getGaps())
156  && $this->getMaximumPoints() > 0) {
157  return true;
158  }
159  return false;
160  }
161 
169  public function cleanQuestiontext($text): string
170  {
171  if ($text === null) {
172  return '';
173  }
174  // fau: fixGapReplace - mask dollars for replacement
175  $text = str_replace('$', 'GAPMASKEDDOLLAR', $text);
176  $text = preg_replace("/\[gap[^\]]*?\]/", "[gap]", $text);
177  $text = preg_replace("/<gap([^>]*?)>/", "[gap]", $text);
178  $text = str_replace("</gap>", "[/gap]", $text);
179  $text = str_replace('GAPMASKEDDOLLAR', '$', $text);
180  // fau.
181  return $text;
182  }
183 
184  // fau: fixGapReplace - add function replaceFirstGap()
191  public function replaceFirstGap($gaptext, $content): string
192  {
193  $content = str_replace('$', 'GAPMASKEDDOLLAR', $content);
194  $output = preg_replace("/\[gap\].*?\[\/gap\]/", $content, $gaptext, 1);
195  $output = str_replace('GAPMASKEDDOLLAR', '$', $output);
196 
197  return $output;
198  }
199  // fau.
206  public function loadFromDb($question_id): void
207  {
208  global $DIC;
209  $ilDB = $DIC['ilDB'];
210  $result = $ilDB->queryF(
211  "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",
212  ["integer"],
213  [$question_id]
214  );
215  if ($result->numRows() == 1) {
216  $data = $ilDB->fetchAssoc($result);
217  $this->setId($question_id);
218  $this->setNrOfTries($data['nr_of_tries']);
219  $this->setObjId($data["obj_fi"]);
220  $this->setTitle((string) $data["title"]);
221  $this->setComment((string) $data["description"]);
222  $this->setOriginalId($data["original_id"]);
223  $this->setAuthor($data["author"]);
224  $this->setPoints($data["points"]);
225  $this->setOwner($data["owner"]);
226  $this->setQuestion($this->cleanQuestiontext($data["question_text"]));
227  $this->setClozeText($data['cloze_text']);
228  $this->setFixedTextLength($data["fixed_textlen"]);
229  $this->setIdenticalScoring(($data['tstamp'] == 0) ? true : $data["identical_scoring"]);
230  $this->setFeedbackMode($data['feedback_mode'] === null ? ilAssClozeTestFeedback::FB_MODE_GAP_QUESTION : $data['feedback_mode']);
231 
232  try {
233  $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
236  }
237 
238  $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1);
239  $this->cloze_text = ilRTE::_replaceMediaObjectImageSrc($this->cloze_text, 1);
240  $this->setTextgapRating($data["textgap_rating"]);
241 
242  try {
243  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
244  } catch (ilTestQuestionPoolException $e) {
245  }
246 
247  $result = $ilDB->queryF(
248  "SELECT * FROM qpl_a_cloze WHERE question_fi = %s ORDER BY gap_id, aorder ASC",
249  ["integer"],
250  [$question_id]
251  );
252  if ($result->numRows() > 0) {
253  $this->gaps = [];
254  while ($data = $ilDB->fetchAssoc($result)) {
255  switch ($data["cloze_type"]) {
256  case CLOZE_TEXT:
257  if (!array_key_exists($data["gap_id"], $this->gaps)) {
258  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_TEXT);
259  }
260  $answer = new assAnswerCloze(
261  $data["answertext"],
262  $data["points"],
263  $data["aorder"]
264  );
265  $this->gaps[$data["gap_id"]]->setGapSize((int) $data['gap_size']);
266 
267  $this->gaps[$data["gap_id"]]->addItem($answer);
268  break;
269  case CLOZE_SELECT:
270  if (!array_key_exists($data["gap_id"], $this->gaps)) {
271  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_SELECT);
272  $this->gaps[$data["gap_id"]]->setShuffle($data["shuffle"]);
273  }
274  $answer = new assAnswerCloze(
275  $data["answertext"],
276  $data["points"],
277  $data["aorder"]
278  );
279  $this->gaps[$data["gap_id"]]->addItem($answer);
280  break;
281  case CLOZE_NUMERIC:
282  if (!array_key_exists($data["gap_id"], $this->gaps)) {
283  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_NUMERIC);
284  }
285  $answer = new assAnswerCloze(
286  $data["answertext"],
287  $data["points"],
288  $data["aorder"]
289  );
290  $this->gaps[$data["gap_id"]]->setGapSize((int) $data['gap_size']);
291  $answer->setLowerBound($data["lowerlimit"]);
292  $answer->setUpperBound($data["upperlimit"]);
293  $this->gaps[$data["gap_id"]]->addItem($answer);
294  break;
295  }
296  }
297  }
298  }
299  $assClozeGapCombinationObj = new assClozeGapCombination();
300  $check_for_gap_combinations = $assClozeGapCombinationObj->loadFromDb($question_id);
301  if (count($check_for_gap_combinations) != 0) {
302  $this->setGapCombinationsExists(true);
303  $this->setGapCombinations($check_for_gap_combinations);
304  }
305  parent::loadFromDb($question_id);
306  }
307 
308  #region Save question to db
309 
319  public function saveToDb($original_id = ""): void
320  {
321  if ($original_id == "") {
322  $this->saveQuestionDataToDb();
323  } else {
325  }
328 
329  parent::saveToDb();
330  }
331 
335  public function saveAnswerSpecificDataToDb()
336  {
337  global $DIC;
338  $ilDB = $DIC['ilDB'];
339 
340  $ilDB->manipulateF(
341  "DELETE FROM qpl_a_cloze WHERE question_fi = %s",
342  [ "integer" ],
343  [ $this->getId() ]
344  );
345 
346  foreach ($this->gaps as $key => $gap) {
347  $this->saveClozeGapItemsToDb($gap, $key);
348  }
349  }
350 
357  {
358  global $DIC; /* @var ILIAS\DI\Container $DIC */
359 
360 
361  $DIC->database()->manipulateF(
362  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
363  [ "integer" ],
364  [ $this->getId() ]
365  );
366 
367  $DIC->database()->insert($this->getAdditionalTableName(), [
368  'question_fi' => ['integer', $this->getId()],
369  'textgap_rating' => ['text', $this->getTextgapRating()],
370  'identical_scoring' => ['text', $this->getIdenticalScoring()],
371  'fixed_textlen' => ['integer', $this->getFixedTextLength() ? $this->getFixedTextLength() : null],
372  'cloze_text' => ['text', ilRTE::_replaceMediaObjectImageSrc($this->getClozeText(), 0)],
373  'feedback_mode' => ['text', $this->getFeedbackMode()]
374  ]);
375  }
376 
383  protected function saveClozeGapItemsToDb($gap, $key): void
384  {
385  global $DIC;
386  $ilDB = $DIC['ilDB'];
387  foreach ($gap->getItems($this->getShuffler()) as $item) {
388  $query = "";
389  $next_id = $ilDB->nextId('qpl_a_cloze');
390  switch ($gap->getType()) {
391  case CLOZE_TEXT:
392  $this->saveClozeTextGapRecordToDb($next_id, $key, $item, $gap);
393  break;
394  case CLOZE_SELECT:
395  $this->saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap);
396  break;
397  case CLOZE_NUMERIC:
398  $this->saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap);
399  break;
400  }
401  }
402  }
403 
412  protected function saveClozeTextGapRecordToDb($next_id, $key, $item, $gap): void
413  {
414  global $DIC;
415  $ilDB = $DIC['ilDB'];
416  $ilDB->manipulateF(
417  "INSERT INTO qpl_a_cloze (answer_id, question_fi, gap_id, answertext, points, aorder, cloze_type, gap_size) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
418  [
419  "integer",
420  "integer",
421  "integer",
422  "text",
423  "float",
424  "integer",
425  "text",
426  "integer"
427  ],
428  [
429  $next_id,
430  $this->getId(),
431  $key,
432  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
433  $item->getPoints(),
434  $item->getOrder(),
435  $gap->getType(),
436  (int) $gap->getGapSize()
437  ]
438  );
439  }
440 
449  protected function saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap): void
450  {
451  global $DIC;
452  $ilDB = $DIC['ilDB'];
453  $ilDB->manipulateF(
454  "INSERT INTO qpl_a_cloze (answer_id, question_fi, gap_id, answertext, points, aorder, cloze_type, shuffle) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
455  [
456  "integer",
457  "integer",
458  "integer",
459  "text",
460  "float",
461  "integer",
462  "text",
463  "text"
464  ],
465  [
466  $next_id,
467  $this->getId(),
468  $key,
469  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
470  $item->getPoints(),
471  $item->getOrder(),
472  $gap->getType(),
473  ($gap->getShuffle()) ? "1" : "0"
474  ]
475  );
476  }
477 
486  protected function saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap): void
487  {
488  global $DIC;
489  $ilDB = $DIC['ilDB'];
490 
491  $eval = new EvalMath();
492  $eval->suppress_errors = true;
493  $ilDB->manipulateF(
494  "INSERT INTO qpl_a_cloze (answer_id, question_fi, gap_id, answertext, points, aorder, cloze_type, lowerlimit, upperlimit, gap_size) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
495  [
496  "integer",
497  "integer",
498  "integer",
499  "text",
500  "float",
501  "integer",
502  "text",
503  "text",
504  "text",
505  "integer"
506  ],
507  [
508  $next_id,
509  $this->getId(),
510  $key,
511  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
512  $item->getPoints(),
513  $item->getOrder(),
514  $gap->getType(),
515  ($eval->e($item->getLowerBound() !== false) && strlen(
516  $item->getLowerBound()
517  ) > 0) ? $item->getLowerBound() : $item->getAnswertext(),
518  ($eval->e($item->getUpperBound() !== false) && strlen(
519  $item->getUpperBound()
520  ) > 0) ? $item->getUpperBound() : $item->getAnswertext(),
521  (int) $gap->getGapSize()
522  ]
523  );
524  }
525 
526 
527 
528  #endregion Save question to db
529 
534  public function getGaps(): array
535  {
536  return $this->gaps;
537  }
538 
539 
544  public function flushGaps(): void
545  {
546  $this->gaps = [];
547  }
548 
558  public function setClozeText($cloze_text = ""): void
559  {
560  $this->gaps = [];
561  $this->cloze_text = $this->cleanQuestiontext($cloze_text);
563  }
564 
565  public function setClozeTextValue($cloze_text = ""): void
566  {
567  $this->cloze_text = $cloze_text;
568  }
569 
577  public function getClozeText(): string
578  {
579  return $this->cloze_text;
580  }
581 
590  public function getClozeTextForHTMLOutput(): string
591  {
592  $gaps = [];
593  preg_match_all('/\[gap\].*?\[\/gap\]/', $this->getClozeText(), $gaps);
594  $string_with_replaced_gaps = str_replace($gaps[0], '######GAP######', $this->getClozeText());
595  $cleaned_text = $this->getHtmlQuestionContentPurifier()->purify(
596  $string_with_replaced_gaps
597  );
598  $cleaned_text_with_gaps = preg_replace_callback('/######GAP######/', function ($match) use (&$gaps) {
599  return array_shift($gaps[0]);
600  }, $cleaned_text);
601 
603  || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
604  $cleaned_text_with_gaps = nl2br($cleaned_text_with_gaps);
605  }
606 
607  return ilLegacyFormElementsUtil::prepareTextareaOutput($cleaned_text_with_gaps, true);
608  }
609 
617  public function getStartTag(): string
618  {
619  return $this->start_tag;
620  }
621 
629  public function setStartTag($start_tag = "[gap]"): void
630  {
631  $this->start_tag = $start_tag;
632  }
633 
641  public function getEndTag(): string
642  {
643  return $this->end_tag;
644  }
645 
653  public function setEndTag($end_tag = "[/gap]"): void
654  {
655  $this->end_tag = $end_tag;
656  }
657 
661  public function getFeedbackMode(): string
662  {
663  return $this->feedbackMode;
664  }
665 
669  public function setFeedbackMode($feedbackMode): void
670  {
671  $this->feedbackMode = $feedbackMode;
672  }
673 
680  public function createGapsFromQuestiontext(): void
681  {
682  $search_pattern = "|\[gap\](.*?)\[/gap\]|i";
683  preg_match_all($search_pattern, $this->getClozeText(), $found);
684  $this->gaps = [];
685  if (count($found[0])) {
686  foreach ($found[1] as $gap_index => $answers) {
687  // create text gaps by default
688  $gap = new assClozeGap(CLOZE_TEXT);
689  $textparams = preg_split("/(?<!\\\\),/", $answers);
690  foreach ($textparams as $key => $value) {
691  $answer = new assAnswerCloze($value, 0, $key);
692  $gap->addItem($answer);
693  }
694  $this->gaps[$gap_index] = $gap;
695  }
696  }
697  }
698 
704  public function setGapType($gap_index, $gap_type): void
705  {
706  if (array_key_exists($gap_index, $this->gaps)) {
707  $this->gaps[$gap_index]->setType($gap_type);
708  }
709  }
710 
720  public function setGapShuffle($gap_index = 0, $shuffle = 1): void
721  {
722  if (array_key_exists($gap_index, $this->gaps)) {
723  $this->gaps[$gap_index]->setShuffle($shuffle);
724  }
725  }
726 
733  public function clearGapAnswers(): void
734  {
735  foreach ($this->gaps as $gap_index => $gap) {
736  $this->gaps[$gap_index]->clearItems();
737  }
738  }
739 
747  public function getGapCount(): int
748  {
749  if (is_array($this->gaps)) {
750  return count($this->gaps);
751  } else {
752  return 0;
753  }
754  }
755 
766  public function addGapAnswer($gap_index, $order, $answer): void
767  {
768  if (array_key_exists($gap_index, $this->gaps)) {
769  if ($this->gaps[$gap_index]->getType() == CLOZE_NUMERIC) {
770  // only allow notation with "." for real numbers
771  $answer = str_replace(",", ".", $answer);
772  }
773  $this->gaps[$gap_index]->addItem(new assAnswerCloze(trim($answer), 0, $order));
774  }
775  }
776 
783  public function getGap($gap_index = 0)
784  {
785  if (array_key_exists($gap_index, $this->gaps)) {
786  return $this->gaps[$gap_index];
787  } else {
788  return null;
789  }
790  }
791 
792  public function setGapSize($gap_index, $size): void
793  {
794  if (array_key_exists($gap_index, $this->gaps)) {
795  $this->gaps[$gap_index]->setGapSize((int) $size);
796  }
797  }
798 
809  public function setGapAnswerPoints($gap_index, $order, $points): void
810  {
811  if (array_key_exists($gap_index, $this->gaps)) {
812  $this->gaps[$gap_index]->setItemPoints($order, $points);
813  }
814  }
815 
824  public function addGapText($gap_index): void
825  {
826  if (array_key_exists($gap_index, $this->gaps)) {
827  $answer = new assAnswerCloze(
828  "",
829  0,
830  $this->gaps[$gap_index]->getItemCount()
831  );
832  $this->gaps[$gap_index]->addItem($answer);
833  }
834  }
835 
844  public function addGapAtIndex($gap, $index): void
845  {
846  $this->gaps[$index] = $gap;
847  }
848 
859  public function setGapAnswerLowerBound($gap_index, $order, $bound): void
860  {
861  if (array_key_exists($gap_index, $this->gaps)) {
862  $this->gaps[$gap_index]->setItemLowerBound($order, $bound);
863  }
864  }
865 
876  public function setGapAnswerUpperBound($gap_index, $order, $bound): void
877  {
878  if (array_key_exists($gap_index, $this->gaps)) {
879  $this->gaps[$gap_index]->setItemUpperBound($order, $bound);
880  }
881  }
882 
889  public function getMaximumPoints(): float
890  {
891  $assClozeGapCombinationObj = new assClozeGapCombination();
892  $points = 0;
893  $gaps_used_in_combination = [];
894  if ($this->gap_combinations_exists) {
895  $points = $assClozeGapCombinationObj->getMaxPointsForCombination($this->getId());
896  $gaps_used_in_combination = $assClozeGapCombinationObj->getGapsWhichAreUsedInCombination($this->getId());
897  }
898  foreach ($this->gaps as $gap_index => $gap) {
899  if (!array_key_exists($gap_index, $gaps_used_in_combination)) {
900  if ($gap->getType() == CLOZE_TEXT) {
901  $gap_max_points = 0;
902  foreach ($gap->getItems($this->getShuffler()) as $item) {
903  if ($item->getPoints() > $gap_max_points) {
904  $gap_max_points = $item->getPoints();
905  }
906  }
907  $points += $gap_max_points;
908  } elseif ($gap->getType() == CLOZE_SELECT) {
909  $srpoints = 0;
910  foreach ($gap->getItems($this->getShuffler()) as $item) {
911  if ($item->getPoints() > $srpoints) {
912  $srpoints = $item->getPoints();
913  }
914  }
915  $points += $srpoints;
916  } elseif ($gap->getType() == CLOZE_NUMERIC) {
917  $numpoints = 0;
918  foreach ($gap->getItems($this->getShuffler()) as $item) {
919  if ($item->getPoints() > $numpoints) {
920  $numpoints = $item->getPoints();
921  }
922  }
923  $points += $numpoints;
924  }
925  }
926  }
927 
928  return $points;
929  }
930 
936  public function duplicate(bool $for_test = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int
937  {
938  if ($this->id <= 0) {
939  // The question has not been saved. It cannot be duplicated
940  return -1;
941  }
942  // duplicate the question in database
943  $this_id = $this->getId();
944  $thisObjId = $this->getObjId();
945 
946  $clone = $this;
947  $original_id = $this->questioninfo->getOriginalId($this->id);
948  $clone->id = -1;
949 
950  if ((int) $testObjId > 0) {
951  $clone->setObjId($testObjId);
952  }
953 
954  if ($title) {
955  $clone->setTitle($title);
956  }
957  if ($author) {
958  $clone->setAuthor($author);
959  }
960  if ($owner) {
961  $clone->setOwner($owner);
962  }
963  if ($for_test) {
964  $clone->saveToDb($original_id);
965  } else {
966  $clone->saveToDb();
967  }
968  if ($this->gap_combinations_exists) {
969  $this->copyGapCombination($this_id, $clone->getId());
970  }
971  if ($for_test) {
972  $clone->saveToDb($original_id);
973  } else {
974  $clone->saveToDb();
975  }
976  // copy question page content
977  $clone->copyPageOfQuestion($this_id);
978  // copy XHTML media objects
979  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
980 
981  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
982 
983  return $clone->getId();
984  }
985 
991  public function copyObject($target_questionpool_id, $title = ""): int
992  {
993  if ($this->getId() <= 0) {
994  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
995  }
996 
997  $thisId = $this->getId();
998  $thisObjId = $this->getObjId();
999 
1000  $clone = $this;
1001  $original_id = $this->questioninfo->getOriginalId($this->getId());
1002  $clone->id = -1;
1003  $clone->setObjId($target_questionpool_id);
1004  if ($title) {
1005  $clone->setTitle($title);
1006  }
1007 
1008  $clone->saveToDb();
1009 
1010  if ($this->gap_combinations_exists) {
1011  $this->copyGapCombination($original_id, $clone->getId());
1012  $clone->saveToDb();
1013  }
1014 
1015  // copy question page content
1016  $clone->copyPageOfQuestion($original_id);
1017  // copy XHTML media objects
1018  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
1019 
1020  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
1021 
1022  return $clone->getId();
1023  }
1024 
1025  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
1026  {
1027  if ($this->getId() <= 0) {
1028  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
1029  }
1030 
1031  $sourceQuestionId = $this->id;
1032  $sourceParentId = $this->getObjId();
1033 
1034  // duplicate the question in database
1035  $clone = $this;
1036  $clone->id = -1;
1037 
1038  $clone->setObjId($targetParentId);
1039 
1040  if ($targetQuestionTitle) {
1041  $clone->setTitle($targetQuestionTitle);
1042  }
1043 
1044  $clone->saveToDb();
1045 
1046  if ($this->gap_combinations_exists) {
1047  $this->copyGapCombination($sourceQuestionId, $clone->getId());
1048  $clone->saveToDb();
1049  }
1050  // copy question page content
1051  $clone->copyPageOfQuestion($sourceQuestionId);
1052  // copy XHTML media objects
1053  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
1054 
1055  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
1056 
1057  return $clone->id;
1058  }
1059 
1060  public function copyGapCombination($orgID, $newID): void
1061  {
1062  $assClozeGapCombinationObj = new assClozeGapCombination();
1063  $array = $assClozeGapCombinationObj->loadFromDb($orgID);
1064  $assClozeGapCombinationObj->importGapCombinationToDb($newID, $array);
1065  }
1066 
1072  public function updateClozeTextFromGaps(): void
1073  {
1074  $output = $this->getClozeText();
1075  foreach ($this->getGaps() as $gap_index => $gap) {
1076  $answers = [];
1077  foreach ($gap->getItemsRaw() as $item) {
1078  array_push($answers, str_replace([',', '['], ["\\,", '[&hairsp;'], $item->getAnswerText()));
1079  }
1080  // fau: fixGapReplace - use replace function
1081  $output = $this->replaceFirstGap($output, "[_gap]" . ilLegacyFormElementsUtil::prepareTextareaOutput(join(",", $answers), true) . "[/_gap]");
1082  // fau.
1083  }
1084  $output = str_replace("_gap]", "gap]", $output);
1085  $this->cloze_text = $output;
1086  }
1087 
1097  public function deleteAnswerText($gap_index, $answer_index): void
1098  {
1099  if (array_key_exists($gap_index, $this->gaps)) {
1100  if ($this->gaps[$gap_index]->getItemCount() == 1) {
1101  // this is the last answer text => remove the gap
1102  $this->deleteGap($gap_index);
1103  } else {
1104  // remove the answer text
1105  $this->gaps[$gap_index]->deleteItem($answer_index);
1106  $this->updateClozeTextFromGaps();
1107  }
1108  }
1109  }
1110 
1119  public function deleteGap($gap_index): void
1120  {
1121  if (array_key_exists($gap_index, $this->gaps)) {
1122  $output = $this->getClozeText();
1123  foreach ($this->getGaps() as $replace_gap_index => $gap) {
1124  $answers = [];
1125  foreach ($gap->getItemsRaw() as $item) {
1126  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
1127  }
1128  if ($replace_gap_index == $gap_index) {
1129  // fau: fixGapReplace - use replace function
1130  $output = $this->replaceFirstGap($output, '');
1131  // fau.
1132  } else {
1133  // fau: fixGapReplace - use replace function
1134  $output = $this->replaceFirstGap($output, "[_gap]" . join(",", $answers) . "[/_gap]");
1135  // fau.
1136  }
1137  }
1138  $output = str_replace("_gap]", "gap]", $output);
1139  $this->cloze_text = $output;
1140  unset($this->gaps[$gap_index]);
1141  $this->gaps = array_values($this->gaps);
1142  }
1143  }
1144 
1154  public function getTextgapPoints($a_original, $a_entered, $max_points): float
1155  {
1156  global $DIC;
1157  $refinery = $DIC->refinery();
1158  $result = 0;
1159  $gaprating = $this->getTextgapRating();
1160 
1161  switch ($gaprating) {
1163  if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) {
1164  $result = $max_points;
1165  }
1166  break;
1168  if (strcmp($a_original, $a_entered) == 0) {
1169  $result = $max_points;
1170  }
1171  break;
1173  $transformation = $refinery->string()->levenshtein()->standard($a_original, 1);
1174  break;
1176  $transformation = $refinery->string()->levenshtein()->standard($a_original, 2);
1177  break;
1179  $transformation = $refinery->string()->levenshtein()->standard($a_original, 3);
1180  break;
1182  $transformation = $refinery->string()->levenshtein()->standard($a_original, 4);
1183  break;
1185  $transformation = $refinery->string()->levenshtein()->standard($a_original, 5);
1186  break;
1187  }
1188 
1189  // run answers against Levenshtein2 methods
1190  if (isset($transformation) && $transformation->transform($a_entered) >= 0) {
1191  $result = $max_points;
1192  }
1193  return $result;
1194  }
1195 
1196 
1206  public function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound): float
1207  {
1208  $eval = new EvalMath();
1209  $eval->suppress_errors = true;
1210  $result = 0.0;
1211 
1212  if ($eval->e($a_entered) === false) {
1213  return 0.0;
1214  } elseif (($eval->e($lowerBound) !== false) && ($eval->e($upperBound) !== false)) {
1215  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
1216  $result = $max_points;
1217  }
1218  } elseif ($eval->e($lowerBound) !== false) {
1219  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) {
1220  $result = $max_points;
1221  }
1222  } elseif ($eval->e($upperBound) !== false) {
1223  if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
1224  $result = $max_points;
1225  }
1226  } elseif ($eval->e($a_entered) == $eval->e($a_original)) {
1227  $result = $max_points;
1228  }
1229  return $result;
1230  }
1231 
1236  public function checkForValidFormula($value): int
1237  {
1238  return preg_match("/^-?(\\d*)(,|\\.|\\/){0,1}(\\d*)$/", $value, $matches);
1239  }
1249  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float|array
1250  {
1251  $ilDB = $this->db;
1252 
1253  if (is_null($pass)) {
1254  $pass = $this->getSolutionMaxPass($active_id);
1255  }
1256 
1257  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
1258  $user_result = [];
1259  while ($data = $ilDB->fetchAssoc($result)) {
1260  if (strcmp($data["value2"], "") != 0) {
1261  $user_result[$data["value1"]] = [
1262  "gap_id" => $data["value1"],
1263  "value" => $data["value2"]
1264  ];
1265  }
1266  }
1267 
1268  ksort($user_result); // this is required when identical scoring for same solutions is disabled
1269 
1270  if ($returndetails) {
1271  $detailed = [];
1272  $this->calculateReachedPointsForSolution($user_result, $detailed);
1273  return $detailed;
1274  }
1275 
1276  return $this->calculateReachedPointsForSolution($user_result);
1277  }
1278 
1279  protected function isValidNumericSubmitValue($submittedValue): bool
1280  {
1281  if (is_numeric($submittedValue)) {
1282  return true;
1283  }
1284 
1285  if (preg_match('/^[-+]{0,1}\d+\/\d+$/', $submittedValue)) {
1286  return true;
1287  }
1288 
1289  return false;
1290  }
1291 
1292  public function validateSolutionSubmit(): bool
1293  {
1294  foreach ($this->getSolutionSubmitValidation() as $gapIndex => $value) {
1295  $gap = $this->getGap($gapIndex);
1296 
1297  if ($gap->getType() != CLOZE_NUMERIC) {
1298  continue;
1299  }
1300 
1301  if (strlen($value) && !$this->isValidNumericSubmitValue($value)) {
1302  $this->tpl->setOnScreenMessage('failure', $this->lng->txt("err_no_numeric_value"), true);
1303  return false;
1304  }
1305  }
1306 
1307  return true;
1308  }
1309 
1310  public function fetchSolutionSubmit($submit): array
1311  {
1312  $solutionSubmit = [];
1313  $post_wrapper = $this->dic->http()->wrapper()->post();
1314  foreach ($this->getGaps() as $index => $gap) {
1315  if (!$post_wrapper->has("gap_$index")) {
1316  continue;
1317  }
1318  $value = trim($post_wrapper->retrieve(
1319  "gap_$index",
1320  $this->dic->refinery()->kindlyTo()->string()
1321  ));
1322  if ($value === '') {
1323  continue;
1324  }
1325 
1326  if (!(($gap->getType() === (int) CLOZE_SELECT) && ($value === -1))) {
1327  if (
1328  $gap->getType() === (int) CLOZE_NUMERIC
1329  && !is_numeric(str_replace(",", ".", $value))
1330  ) {
1331  $value = null;
1332  } elseif ($gap->getType() === (int) CLOZE_NUMERIC) {
1333  $value = str_replace(",", ".", $value);
1334  }
1335  $solutionSubmit[$index] = $value;
1336  }
1337  }
1338 
1339  return $solutionSubmit;
1340  }
1341 
1342  public function getSolutionSubmitValidation(): array
1343  {
1344  $submit = $_POST;
1345  $solutionSubmit = [];
1346 
1347  foreach ($submit as $key => $value) {
1348  if (preg_match("/^gap_(\d+)/", $key, $matches)) {
1349  if ($value !== null && $value !== '') {
1350  $gap = $this->getGap($matches[1]);
1351  if (is_object($gap)) {
1352  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1))) {
1353  if ($gap->getType() == CLOZE_NUMERIC) {
1354  $value = str_replace(",", ".", $value);
1355  }
1356  $solutionSubmit[trim($matches[1])] = $value;
1357  }
1358  }
1359  }
1360  }
1361  }
1362 
1363  return $solutionSubmit;
1364  }
1365 
1366  public function getSolutionSubmit(): array
1367  {
1368  return $this->fetchSolutionSubmit($_POST);
1369  }
1370 
1379  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
1380  {
1381  if (is_null($pass)) {
1382  $pass = ilObjTest::_getPass($active_id);
1383  }
1384 
1385  $entered_values = 0;
1386 
1387  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
1388  $this->removeCurrentSolution($active_id, $pass, $authorized);
1389 
1390  foreach ($this->getSolutionSubmit() as $key => $value) {
1391  if ($value !== null && $value !== '') {
1392  $gap = $this->getGap(trim(ilUtil::stripSlashes($key)));
1393  if (is_object($gap)) {
1394  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1))) {
1395  $this->saveCurrentSolution($active_id, $pass, $key, $value, $authorized);
1396  $entered_values++;
1397  }
1398  }
1399  }
1400  }
1401  });
1402 
1403  if ($entered_values) {
1405  assQuestion::logAction($this->lng->txtlng(
1406  "assessment",
1407  "log_user_entered_values",
1409  ), $active_id, $this->getId());
1410  }
1411  } else {
1413  assQuestion::logAction($this->lng->txtlng(
1414  "assessment",
1415  "log_user_not_entered_values",
1417  ), $active_id, $this->getId());
1418  }
1419  }
1420 
1421  return true;
1422  }
1423 
1430  public function getQuestionType(): string
1431  {
1432  return "assClozeTest";
1433  }
1434 
1442  public function getTextgapRating(): string
1443  {
1444  return $this->textgap_rating;
1445  }
1446 
1454  public function setTextgapRating($a_textgap_rating): void
1455  {
1456  switch ($a_textgap_rating) {
1464  $this->textgap_rating = $a_textgap_rating;
1465  break;
1466  default:
1467  $this->textgap_rating = TEXTGAP_RATING_CASEINSENSITIVE;
1468  break;
1469  }
1470  }
1471 
1479  public function getIdenticalScoring()
1480  {
1481  return ($this->identical_scoring) ? 1 : 0;
1482  }
1483 
1491  public function setIdenticalScoring($a_identical_scoring): void
1492  {
1493  $this->identical_scoring = ($a_identical_scoring) ? 1 : 0;
1494  }
1495 
1502  public function getAdditionalTableName(): string
1503  {
1504  return "qpl_qst_cloze";
1505  }
1506 
1507  public function getAnswerTableName(): array
1508  {
1509  return ["qpl_a_cloze",'qpl_a_cloze_combi_res'];
1510  }
1511 
1518  public function setFixedTextLength($a_text_len): void
1519  {
1520  $this->fixedTextLength = $a_text_len;
1521  }
1522 
1529  public function getFixedTextLength()
1530  {
1531  return $this->fixedTextLength;
1532  }
1533 
1542  public function getMaximumGapPoints($gap_index)
1543  {
1544  $points = 0;
1545  $gap_max_points = 0;
1546  if (array_key_exists($gap_index, $this->gaps)) {
1547  $gap = &$this->gaps[$gap_index];
1548  foreach ($gap->getItems($this->getShuffler()) as $answer) {
1549  if ($answer->getPoints() > $gap_max_points) {
1550  $gap_max_points = $answer->getPoints();
1551  }
1552  }
1553  $points += $gap_max_points;
1554  }
1555  return $points;
1556  }
1557 
1562  public function getRTETextWithMediaObjects(): string
1563  {
1564  return parent::getRTETextWithMediaObjects() . $this->getClozeText();
1565  }
1566  public function getGapCombinationsExists(): bool
1567  {
1569  }
1570 
1571  public function getGapCombinations(): array
1572  {
1573  return $this->gap_combinations;
1574  }
1575 
1576  public function setGapCombinationsExists($value): void
1577  {
1578  $this->gap_combinations_exists = $value;
1579  }
1580 
1581  public function setGapCombinations($value): void
1582  {
1583  $this->gap_combinations = $value;
1584  }
1585 
1589  public function setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass): int
1590  {
1591  parent::setExportDetailsXLSX($worksheet, $startrow, $col, $active_id, $pass);
1592 
1593  $solution = $this->getSolutionValues($active_id, $pass);
1594  $i = 1;
1595  foreach ($this->getGaps() as $gap_index => $gap) {
1596  $worksheet->setCell($startrow + $i, $col, $this->lng->txt("gap") . " $i");
1597  $worksheet->setBold($worksheet->getColumnCoord($col) . ($startrow + $i));
1598  $checked = false;
1599  foreach ($solution as $solutionvalue) {
1600  if ($gap_index == $solutionvalue["value1"]) {
1601  $string_escaping_org_value = $worksheet->getStringEscaping();
1602  try {
1603  $worksheet->setStringEscaping(false);
1604 
1605  switch ($gap->getType()) {
1606  case CLOZE_SELECT:
1607  $worksheet->setCell($startrow + $i, $col + 2, $gap->getItem($solutionvalue["value2"])->getAnswertext());
1608  break;
1609  case CLOZE_NUMERIC:
1610  case CLOZE_TEXT:
1611  $worksheet->setCell($startrow + $i, $col + 2, $solutionvalue["value2"]);
1612  break;
1613  }
1614  } finally {
1615  $worksheet->setStringEscaping($string_escaping_org_value);
1616  }
1617  }
1618  }
1619  $i++;
1620  }
1621 
1622  return $startrow + $i + 1;
1623  }
1624 
1629  {
1630  // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1631  //$this->setClozeText( $migrator->migrateToLmContent($this->getClozeText()) );
1632  $this->cloze_text = $migrator->migrateToLmContent($this->getClozeText());
1633  // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1634  }
1635 
1639  public function toJSON(): string
1640  {
1641  $result = [];
1642  $result['id'] = $this->getId();
1643  $result['type'] = (string) $this->getQuestionType();
1644  $result['title'] = $this->getTitleForHTMLOutput();
1645  $result['question'] = $this->formatSAQuestion($this->getQuestion());
1646  $result['clozetext'] = $this->formatSAQuestion($this->getClozeText());
1647  $result['nr_of_tries'] = $this->getNrOfTries();
1648  $result['shuffle'] = $this->getShuffle();
1649  $result['feedback'] = [
1650  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1651  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1652  ];
1653 
1654  $gaps = [];
1655  foreach ($this->getGaps() as $key => $gap) {
1656  $items = [];
1657  foreach ($gap->getItems($this->getShuffler()) as $item) {
1658  $jitem = [];
1659  $jitem['points'] = $item->getPoints();
1660  $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1661  $jitem['order'] = $item->getOrder();
1662  if ($gap->getType() == CLOZE_NUMERIC) {
1663  $jitem['lowerbound'] = $item->getLowerBound();
1664  $jitem['upperbound'] = $item->getUpperBound();
1665  } else {
1666  $jitem['value'] = trim($jitem['value']);
1667  }
1668  array_push($items, $jitem);
1669  }
1670 
1671  if ($gap->getGapSize() && ($gap->getType() == CLOZE_TEXT || $gap->getType() == CLOZE_NUMERIC)) {
1672  $jgap['size'] = $gap->getGapSize();
1673  }
1674 
1675  $jgap['shuffle'] = $gap->getShuffle();
1676  $jgap['type'] = $gap->getType();
1677  $jgap['item'] = $items;
1678 
1679  array_push($gaps, $jgap);
1680  }
1681  $result['gaps'] = $gaps;
1682  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1683  $result['mobs'] = $mobs;
1684  return json_encode($result);
1685  }
1686 
1695  public function getOperators($expression): array
1696  {
1698  }
1699 
1704  public function getExpressionTypes(): array
1705  {
1706  return [
1712  ];
1713  }
1714 
1723  public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
1724  {
1726  global $DIC;
1727  $ilDB = $DIC['ilDB'];
1728  $result = new ilUserQuestionResult($this, $active_id, $pass);
1729 
1730  $maxStep = $this->lookupMaxStep($active_id, $pass);
1731 
1732  if ($maxStep > 0) {
1733  $data = $ilDB->queryF(
1734  "
1735  SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1736  FROM tst_solutions sol
1737  INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1738  WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s AND sol.step = %s
1739  GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1740  ",
1741  ["integer", "integer", "integer","integer"],
1742  [$active_id, $pass, $this->getId(), $maxStep]
1743  );
1744  } else {
1745  $data = $ilDB->queryF(
1746  "
1747  SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1748  FROM tst_solutions sol
1749  INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1750  WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s
1751  GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1752  ",
1753  ["integer", "integer", "integer"],
1754  [$active_id, $pass, $this->getId()]
1755  );
1756  }
1757 
1758  while ($row = $ilDB->fetchAssoc($data)) {
1759  if ($row["cloze_type"] == 1) {
1760  $row["value2"]++;
1761  }
1762  $result->addKeyValue($row["val"], $row["value2"]);
1763  }
1764 
1765  $points = $this->calculateReachedPoints($active_id, $pass);
1766  $max_points = $this->getMaximumPoints();
1767 
1768  $result->setReachedPercentage(($points / $max_points) * 100);
1769 
1770  return $result;
1771  }
1772 
1781  public function getAvailableAnswerOptions($index = null)
1782  {
1783  if ($index !== null) {
1784  return $this->getGap($index);
1785  } else {
1786  return $this->getGaps();
1787  }
1788  }
1789 
1790  public function calculateCombinationResult($user_result): array
1791  {
1792  $points = 0;
1793 
1794  $assClozeGapCombinationObj = new assClozeGapCombination();
1795  $gap_used_in_combination = [];
1796  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1797  $combinations_for_question = $assClozeGapCombinationObj->getCleanCombinationArray($this->getId());
1798  $gap_answers = [];
1799 
1800  foreach ($user_result as $user_result_build_list) {
1801  if (is_array($user_result_build_list)) {
1802  $gap_answers[$user_result_build_list['gap_id']] = $user_result_build_list['value'];
1803  }
1804  }
1805 
1806  foreach ($combinations_for_question as $combination) {
1807  foreach ($combination as $row_key => $row_answers) {
1808  $combination_fulfilled = true;
1809  $points_for_combination = $row_answers['points'];
1810  foreach ($row_answers as $gap_key => $combination_gap_answer) {
1811  if ($gap_key !== 'points') {
1812  $gap_used_in_combination[$gap_key] = $gap_key;
1813  }
1814  if ($combination_fulfilled && array_key_exists($gap_key, $gap_answers)) {
1815  switch ($combination_gap_answer['type']) {
1816  case CLOZE_TEXT:
1817  $is_text_gap_correct = $this->getTextgapPoints($gap_answers[$gap_key], $combination_gap_answer['answer'], 1);
1818  if ($is_text_gap_correct != 1) {
1819  $combination_fulfilled = false;
1820  }
1821  break;
1822  case CLOZE_SELECT:
1823  $answer = $this->gaps[$gap_key]->getItem($gap_answers[$gap_key]);
1824  $answertext = $answer?->getAnswertext();
1825  if ($answertext != $combination_gap_answer['answer']) {
1826  $combination_fulfilled = false;
1827  }
1828  break;
1829  case CLOZE_NUMERIC:
1830  $answer = $this->gaps[$gap_key]->getItem(0);
1831  if ($combination_gap_answer['answer'] != 'out_of_bound') {
1832  $is_numeric_gap_correct = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1833  if ($is_numeric_gap_correct != 1) {
1834  $combination_fulfilled = false;
1835  }
1836  } else {
1837  $wrong_is_the_new_right = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1838  if ($wrong_is_the_new_right == 1) {
1839  $combination_fulfilled = false;
1840  }
1841  }
1842  break;
1843  }
1844  } else {
1845  if ($gap_key !== 'points') {
1846  $combination_fulfilled = false;
1847  }
1848  }
1849  }
1850  if ($combination_fulfilled) {
1851  $points += $points_for_combination;
1852  }
1853  }
1854  }
1855  }
1856  return [$points, $gap_used_in_combination];
1857  }
1862  protected function calculateReachedPointsForSolution($user_result, &$detailed = null): float
1863  {
1864  if ($detailed === null) {
1865  $detailed = [];
1866  }
1867 
1868  $points = 0;
1869 
1870  $assClozeGapCombinationObj = new assClozeGapCombination();
1871  $combinations[1] = [];
1872  if ($this->gap_combinations_exists) {
1873  $combinations = $this->calculateCombinationResult($user_result);
1874  $points = $combinations[0];
1875  }
1876  $counter = 0;
1877  $solution_values_text = []; // for identical scoring checks
1878  $solution_values_select = []; // for identical scoring checks
1879  $solution_values_numeric = []; // for identical scoring checks
1880  foreach ($user_result as $gap_id => $value) {
1881  if (is_string($value)) {
1882  $value = ["value" => $value];
1883  }
1884 
1885  if (array_key_exists($gap_id, $this->gaps) && !array_key_exists($gap_id, $combinations[1])) {
1886  switch ($this->gaps[$gap_id]->getType()) {
1887  case CLOZE_TEXT:
1888  $gappoints = 0;
1889  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1890  $answer = $this->gaps[$gap_id]->getItem($order);
1891  $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1892  if ($gotpoints > $gappoints) {
1893  $gappoints = $gotpoints;
1894  }
1895  }
1896  if (!$this->getIdenticalScoring()) {
1897  // check if the same solution text was already entered
1898  if ((in_array($value["value"], $solution_values_text)) && ($gappoints > 0)) {
1899  $gappoints = 0;
1900  }
1901  }
1902  $points += $gappoints;
1903  $detailed[$gap_id] = ["points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0) ? true : false];
1904  array_push($solution_values_text, $value["value"]);
1905  break;
1906  case CLOZE_NUMERIC:
1907  $gappoints = 0;
1908  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1909  $answer = $this->gaps[$gap_id]->getItem($order);
1910  $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1911  if ($gotpoints > $gappoints) {
1912  $gappoints = $gotpoints;
1913  }
1914  }
1915  if (!$this->getIdenticalScoring()) {
1916  // check if the same solution value was already entered
1917  $eval = new EvalMath();
1918  $eval->suppress_errors = true;
1919  $found_value = false;
1920  foreach ($solution_values_numeric as $solval) {
1921  if ($eval->e($solval) == $eval->e($value["value"])) {
1922  $found_value = true;
1923  }
1924  }
1925  if ($found_value && ($gappoints > 0)) {
1926  $gappoints = 0;
1927  }
1928  }
1929  $points += $gappoints;
1930  $detailed[$gap_id] = ["points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0) ? true : false];
1931  array_push($solution_values_numeric, $value["value"]);
1932  break;
1933  case CLOZE_SELECT:
1934  if ($value["value"] >= 0) {
1935  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1936  $answer = $this->gaps[$gap_id]->getItem($order);
1937  if ($value["value"] == $answer->getOrder()) {
1938  $answerpoints = $answer->getPoints();
1939  if (!$this->getIdenticalScoring()) {
1940  // check if the same solution value was already entered
1941  if ((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0)) {
1942  $answerpoints = 0;
1943  }
1944  }
1945  $points += $answerpoints;
1946  $detailed[$gap_id] = ["points" => $answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? true : false, "positive" => ($answerpoints > 0) ? true : false];
1947  array_push($solution_values_select, $answer->getAnswertext());
1948  }
1949  }
1950  }
1951  break;
1952  }
1953  }
1954  }
1955 
1956  return (float) $points;
1957  }
1958 
1960  {
1961  $participant_session = $preview_session->getParticipantsSolution();
1962 
1963  if (!is_array($participant_session)) {
1964  return 0;
1965  }
1966 
1967  $user_solution = [];
1968 
1969  foreach ($participant_session as $key => $val) {
1970  $user_solution[$key] = ['gap_id' => $key, 'value' => $val];
1971  }
1972 
1973  $reached_points = $this->calculateReachedPointsForSolution($user_solution);
1974  $reached_points = $this->deductHintPointsFromReachedPoints($preview_session, $reached_points);
1975 
1976  return $this->ensureNonNegativePoints($reached_points);
1977  }
1978 
1979  public function fetchAnswerValueForGap($userSolution, $gapIndex): string
1980  {
1981  $answerValue = '';
1982 
1983  foreach ($userSolution as $value1 => $value2) {
1984  if ($value1 == $gapIndex) {
1985  $answerValue = $value2;
1986  break;
1987  }
1988  }
1989 
1990  return $answerValue;
1991  }
1992 
1993  public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
1994  {
1995  $gap = $this->getGap($qIndex);
1996 
1997  if ($gap->getType() != CLOZE_TEXT) {
1998  return false;
1999  }
2000 
2001  foreach ($gap->getItems($this->randomGroup->dontShuffle()) as $item) {
2002  if ($item->getAnswertext() === $answerOptionValue) {
2003  return false;
2004  }
2005  }
2006 
2007  return true;
2008  }
2009 
2010  public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
2011  {
2012  $gap = $this->getGap($qIndex); /* @var assClozeGap $gap */
2013 
2014  $item = new assAnswerCloze($answerOptionValue, $points);
2015  $item->setOrder($gap->getItemCount());
2016 
2017  $gap->addItem($item);
2018  }
2019 }
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.
setNrOfTries(int $a_nr_of_tries)
fetchSolutionSubmit($submit)
toJSON()
Returns a JSON representation of the question.
getGapCount()
Returns the number of gaps.
setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass)
{}
const TEXTGAP_RATING_LEVENSHTEIN5
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getMaximumGapPoints($gap_index)
Returns the maximum points for a gap.
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
getQuestionType()
Returns the question type of the question.
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...
getClozeTextForHTMLOutput()
Returns the cloze text as HTML (with optional nl2br) Fix for Mantis 29987: We assume Tiny embeds any ...
setEndTag($end_tag="[/gap]")
Sets the end tag of a cloze gap.
clearGapAnswers()
Removes all answers from the gaps.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getAdditionalTableName()
Returns the name of the additional question data table in the database.
saveAnswerSpecificDataToDb()
Save all gaps to the database.
setGapSize($gap_index, $size)
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const TEXTGAP_RATING_LEVENSHTEIN2
const TEXTGAP_RATING_LEVENSHTEIN1
Abstract basic class which is to be extended by the concrete assessment question type classes...
const CLOZE_TEXT
Cloze question constants.
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
setOwner(int $owner=-1)
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
calculateCombinationResult($user_result)
getOperators($expression)
Get all available operations for a specific question.
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
getColumnCoord(int $a_col)
Get column "name" from number.
ensureNonNegativePoints($points)
bool $shuffle
Indicates whether the answers will be shuffled or not.
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
RandomGroup $randomGroup
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
copyGapCombination($orgID, $newID)
getGap($gap_index=0)
Returns the gap at a given index.
setGapAnswerPoints($gap_index, $order, $points)
Sets the points of a gap with a given index and an answer with a given order.
getTextgapRating()
Returns the rating option for text gaps.
Class for cloze question gaps.
saveToDb($original_id="")
Saves a assClozeTest object to a database.
setClozeTextValue($cloze_text="")
setCell($a_row, $a_col, $a_value, $datatype=null)
getUserQuestionResult($active_id, $pass)
Get the user solution for a question by active_id and the test pass.
checkForValidFormula($value)
setComment(string $comment="")
createGapsFromQuestiontext()
Create gap entries by parsing the question text.
float $points
The maximum available points for the question.
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound)
Returns the points for a text gap and compares the given solution with the entered solution using the...
ilAssQuestionFeedback $feedbackOBJ
getStartTag()
Returns the start tag of a cloze gap.
global $DIC
Definition: feed.php:28
getTextgapPoints($a_original, $a_entered, $max_points)
Returns the points for a text gap and compares the given solution with the entered solution using the...
setGapType($gap_index, $gap_type)
Set the type of a gap with a given index.
setGapCombinationsExists($value)
getGaps()
Returns the array of gaps.
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
setBold(string $a_coords)
Set cell(s) to bold.
const CLOZE_SELECT
__construct(VocabulariesInterface $vocabularies)
const TEXTGAP_RATING_LEVENSHTEIN3
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setGapAnswerLowerBound($gap_index, $order, $bound)
Sets the lower bound of a gap with a given index and an answer with a given order.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static logAction(string $logtext, int $active_id, int $question_id)
setFixedTextLength($a_text_len)
Sets a fixed text length for all text fields in the cloze question.
saveWorkingData($active_id, $pass=null, $authorized=true)
Saves the learners input of the question to the database.
isValidNumericSubmitValue($submittedValue)
copyObject($target_questionpool_id, $title="")
Copies an assClozeTest object.
const TEXTGAP_RATING_CASESENSITIVE
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
string $key
Consumer key/client ID value.
Definition: System.php:193
setPoints(float $points)
setObjId(int $obj_id=0)
addGapAtIndex($gap, $index)
Adds a ClozeGap object at a given index.
string $question
The question text.
setGapAnswerUpperBound($gap_index, $order, $bound)
Sets the upper bound of a gap with a given index and an answer with a given order.
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
saveClozeTextGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
saveAdditionalQuestionDataToDb()
Saves the data for the additional data table.
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
addGapAnswer($gap_index, $order, $answer)
Sets the answer text of a gap with a given index.
setGapShuffle($gap_index=0, $shuffle=1)
Sets the shuffle state of a gap with a given index.
setClozeText($cloze_text="")
Evaluates the text gap solutions from the cloze text.
cleanQuestiontext($text)
Cleans cloze question text to remove attributes or tags from older ILIAS versions.
getExpressionTypes()
Get all available expression types for a specific question.
fetchAnswerValueForGap($userSolution, $gapIndex)
flushGaps()
Deletes all gaps without changing the cloze text.
isAdditionalContentEditingModePageObject()
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
saveQuestionDataToDb(int $original_id=-1)
deleteGap($gap_index)
Deletes a gap with a given index.
getSolutionMaxPass(int $active_id)
replaceFirstGap($gaptext, $content)
Replace the first gap in a string without treating backreferences.
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
updateClozeTextFromGaps()
Updates the gap parameters in the cloze text from the form input.
setFeedbackMode($feedbackMode)
setTextgapRating($a_textgap_rating)
Sets the rating option for text gaps.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getEndTag()
Returns the end tag of a cloze gap.
setId(int $id=-1)
ilDBInterface $db
setStartTag($start_tag="[gap]")
Sets the start tag of a cloze gap.
addGapText($gap_index)
Adds a new answer text value to a text gap with a given index.
deleteAnswerText($gap_index, $answer_index)
Deletes the answer text of a gap with a given index and an answer with a given order.
getFixedTextLength()
Gets the fixed text length for all text fields in the cloze question.
setOriginalId(?int $original_id)
const TEXTGAP_RATING_LEVENSHTEIN4
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setTitle(string $title="")
static strToLower(string $a_string)
Definition: class.ilStr.php:72
setLifecycle(ilAssQuestionLifecycle $lifecycle)
saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
getClozeText()
Returns the cloze text.
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
__construct( $title="", $comment="", $author="", $owner=-1, $question="")
assClozeTest constructor
loadFromDb($question_id)
Loads a assClozeTest object from a database.
const CLOZE_NUMERIC
isComplete()
Returns TRUE, if a cloze test is complete for use.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
lookupMaxStep(int $active_id, int $pass)
getIdenticalScoring()
Returns the identical scoring status of the question.
setAuthor(string $author="")
static prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free...
setIdenticalScoring($a_identical_scoring)
Sets the identical scoring option for cloze questions.
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
const FB_MODE_GAP_QUESTION
constants for different feedback modes (per gap or per gap-answers/options)
saveClozeGapItemsToDb($gap, $key)
Save all items belonging to one cloze gap to the db.
calculateReachedPointsForSolution($user_result, &$detailed=null)
ILIAS Refinery Factory $refinery
duplicate(bool $for_test=true, string $title="", string $author="", int $owner=-1, $testObjId=null)
Duplicates an assClozeTest.
const TEXTGAP_RATING_CASEINSENSITIVE
setQuestion(string $question="")