ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
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  // fau: fixGapReplace - mask dollars for replacement
172  $text = str_replace('$', 'GAPMASKEDDOLLAR', $text);
173  $text = preg_replace("/\[gap[^\]]*?\]/", "[gap]", $text);
174  $text = preg_replace("/<gap([^>]*?)>/", "[gap]", $text);
175  $text = str_replace("</gap>", "[/gap]", $text);
176  $text = str_replace('GAPMASKEDDOLLAR', '$', $text);
177  // fau.
178  return $text;
179  }
180 
181  // fau: fixGapReplace - add function replaceFirstGap()
188  public function replaceFirstGap($gaptext, $content): string
189  {
190  $content = str_replace('$', 'GAPMASKEDDOLLAR', $content);
191  $output = preg_replace("/\[gap\].*?\[\/gap\]/", $content, $gaptext, 1);
192  $output = str_replace('GAPMASKEDDOLLAR', '$', $output);
193 
194  return $output;
195  }
196  // fau.
203  public function loadFromDb($question_id): void
204  {
205  global $DIC;
206  $ilDB = $DIC['ilDB'];
207  $result = $ilDB->queryF(
208  "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",
209  array("integer"),
210  array($question_id)
211  );
212  if ($result->numRows() == 1) {
213  $data = $ilDB->fetchAssoc($result);
214  $this->setId($question_id);
215  $this->setNrOfTries($data['nr_of_tries']);
216  $this->setObjId($data["obj_fi"]);
217  $this->setTitle((string) $data["title"]);
218  $this->setComment((string) $data["description"]);
219  $this->setOriginalId($data["original_id"]);
220  $this->setAuthor($data["author"]);
221  $this->setPoints($data["points"]);
222  $this->setOwner($data["owner"]);
223  $this->setQuestion($this->cleanQuestiontext($data["question_text"]));
224  $this->setClozeText($data['cloze_text']);
225  $this->setFixedTextLength($data["fixed_textlen"]);
226  $this->setIdenticalScoring(($data['tstamp'] == 0) ? true : $data["identical_scoring"]);
227  $this->setFeedbackMode($data['feedback_mode'] === null ? ilAssClozeTestFeedback::FB_MODE_GAP_QUESTION : $data['feedback_mode']);
228 
229  try {
230  $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
233  }
234 
235  // replacement of old syntax with new syntax
236  include_once("./Services/RTE/classes/class.ilRTE.php");
237  $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1);
238  $this->cloze_text = ilRTE::_replaceMediaObjectImageSrc($this->cloze_text, 1);
239  $this->setTextgapRating($data["textgap_rating"]);
240 
241  try {
242  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
243  } catch (ilTestQuestionPoolException $e) {
244  }
245 
246  // open the cloze gaps with all answers
247  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
248  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
249  $result = $ilDB->queryF(
250  "SELECT * FROM qpl_a_cloze WHERE question_fi = %s ORDER BY gap_id, aorder ASC",
251  array("integer"),
252  array($question_id)
253  );
254  if ($result->numRows() > 0) {
255  $this->gaps = [];
256  while ($data = $ilDB->fetchAssoc($result)) {
257  switch ($data["cloze_type"]) {
258  case CLOZE_TEXT:
259  if (!array_key_exists($data["gap_id"], $this->gaps)) {
260  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_TEXT);
261  }
262  $answer = new assAnswerCloze(
263  $data["answertext"],
264  $data["points"],
265  $data["aorder"]
266  );
267  $this->gaps[$data["gap_id"]]->setGapSize((int) $data['gap_size']);
268 
269  $this->gaps[$data["gap_id"]]->addItem($answer);
270  break;
271  case CLOZE_SELECT:
272  if (!array_key_exists($data["gap_id"], $this->gaps)) {
273  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_SELECT);
274  $this->gaps[$data["gap_id"]]->setShuffle($data["shuffle"]);
275  }
276  $answer = new assAnswerCloze(
277  $data["answertext"],
278  $data["points"],
279  $data["aorder"]
280  );
281  $this->gaps[$data["gap_id"]]->addItem($answer);
282  break;
283  case CLOZE_NUMERIC:
284  if (!array_key_exists($data["gap_id"], $this->gaps)) {
285  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_NUMERIC);
286  }
287  $answer = new assAnswerCloze(
288  $data["answertext"],
289  $data["points"],
290  $data["aorder"]
291  );
292  $this->gaps[$data["gap_id"]]->setGapSize((int) $data['gap_size']);
293  $answer->setLowerBound($data["lowerlimit"]);
294  $answer->setUpperBound($data["upperlimit"]);
295  $this->gaps[$data["gap_id"]]->addItem($answer);
296  break;
297  }
298  }
299  }
300  }
301  $assClozeGapCombinationObj = new assClozeGapCombination();
302  $check_for_gap_combinations = $assClozeGapCombinationObj->loadFromDb($question_id);
303  if (count($check_for_gap_combinations) != 0) {
304  $this->setGapCombinationsExists(true);
305  $this->setGapCombinations($check_for_gap_combinations);
306  }
307  parent::loadFromDb($question_id);
308  }
309 
310  #region Save question to db
311 
321  public function saveToDb($original_id = ""): void
322  {
323  if ($original_id == "") {
324  $this->saveQuestionDataToDb();
325  } else {
327  }
330 
331  parent::saveToDb();
332  }
333 
337  public function saveAnswerSpecificDataToDb()
338  {
339  global $DIC;
340  $ilDB = $DIC['ilDB'];
341 
342  $ilDB->manipulateF(
343  "DELETE FROM qpl_a_cloze WHERE question_fi = %s",
344  array( "integer" ),
345  array( $this->getId() )
346  );
347 
348  foreach ($this->gaps as $key => $gap) {
349  $this->saveClozeGapItemsToDb($gap, $key);
350  }
351  }
352 
359  {
360  global $DIC; /* @var ILIAS\DI\Container $DIC */
361 
362 
363  $DIC->database()->manipulateF(
364  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
365  array( "integer" ),
366  array( $this->getId() )
367  );
368 
369  $DIC->database()->insert($this->getAdditionalTableName(), array(
370  'question_fi' => array('integer', $this->getId()),
371  'textgap_rating' => array('text', $this->getTextgapRating()),
372  'identical_scoring' => array('text', $this->getIdenticalScoring()),
373  'fixed_textlen' => array('integer', $this->getFixedTextLength() ? $this->getFixedTextLength() : null),
374  'cloze_text' => array('text', ilRTE::_replaceMediaObjectImageSrc($this->getClozeText(), 0)),
375  'feedback_mode' => array('text', $this->getFeedbackMode())
376  ));
377  }
378 
385  protected function saveClozeGapItemsToDb($gap, $key): void
386  {
387  global $DIC;
388  $ilDB = $DIC['ilDB'];
389  foreach ($gap->getItems($this->getShuffler()) as $item) {
390  $query = "";
391  $next_id = $ilDB->nextId('qpl_a_cloze');
392  switch ($gap->getType()) {
393  case CLOZE_TEXT:
394  $this->saveClozeTextGapRecordToDb($next_id, $key, $item, $gap);
395  break;
396  case CLOZE_SELECT:
397  $this->saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap);
398  break;
399  case CLOZE_NUMERIC:
400  $this->saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap);
401  break;
402  }
403  }
404  }
405 
414  protected function saveClozeTextGapRecordToDb($next_id, $key, $item, $gap): void
415  {
416  global $DIC;
417  $ilDB = $DIC['ilDB'];
418  $ilDB->manipulateF(
419  "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)",
420  array(
421  "integer",
422  "integer",
423  "integer",
424  "text",
425  "float",
426  "integer",
427  "text",
428  "integer"
429  ),
430  array(
431  $next_id,
432  $this->getId(),
433  $key,
434  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
435  $item->getPoints(),
436  $item->getOrder(),
437  $gap->getType(),
438  (int) $gap->getGapSize()
439  )
440  );
441  }
442 
451  protected function saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap): void
452  {
453  global $DIC;
454  $ilDB = $DIC['ilDB'];
455  $ilDB->manipulateF(
456  "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)",
457  [
458  "integer",
459  "integer",
460  "integer",
461  "text",
462  "float",
463  "integer",
464  "text",
465  "text"
466  ],
467  [
468  $next_id,
469  $this->getId(),
470  $key,
471  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
472  $item->getPoints(),
473  $item->getOrder(),
474  $gap->getType(),
475  ($gap->getShuffle()) ? "1" : "0"
476  ]
477  );
478  }
479 
488  protected function saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap): void
489  {
490  global $DIC;
491  $ilDB = $DIC['ilDB'];
492 
493  include_once "./Services/Math/classes/class.EvalMath.php";
494  $eval = new EvalMath();
495  $eval->suppress_errors = true;
496  $ilDB->manipulateF(
497  "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)",
498  array(
499  "integer",
500  "integer",
501  "integer",
502  "text",
503  "float",
504  "integer",
505  "text",
506  "text",
507  "text",
508  "integer"
509  ),
510  array(
511  $next_id,
512  $this->getId(),
513  $key,
514  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
515  $item->getPoints(),
516  $item->getOrder(),
517  $gap->getType(),
518  ($eval->e($item->getLowerBound() !== false) && strlen(
519  $item->getLowerBound()
520  ) > 0) ? $item->getLowerBound() : $item->getAnswertext(),
521  ($eval->e($item->getUpperBound() !== false) && strlen(
522  $item->getUpperBound()
523  ) > 0) ? $item->getUpperBound() : $item->getAnswertext(),
524  (int) $gap->getGapSize()
525  )
526  );
527  }
528 
529 
530 
531  #endregion Save question to db
532 
537  public function getGaps(): array
538  {
539  return $this->gaps;
540  }
541 
542 
547  public function flushGaps(): void
548  {
549  $this->gaps = [];
550  }
551 
561  public function setClozeText($cloze_text = ""): void
562  {
563  $this->gaps = [];
564  $this->cloze_text = $this->cleanQuestiontext($cloze_text);
566  }
567 
568  public function setClozeTextValue($cloze_text = ""): void
569  {
570  $this->cloze_text = $cloze_text;
571  }
572 
580  public function getClozeText(): string
581  {
582  return $this->cloze_text;
583  }
584 
593  public function getClozeTextForHTMLOutput(): string
594  {
595  $gaps = [];
596  preg_match_all('/\[gap\].*?\[\/gap\]/', $this->getClozeText(), $gaps);
597  $string_with_replaced_gaps = str_replace($gaps[0], '######GAP######', $this->getClozeText());
598  $cleaned_text = $this->getHtmlQuestionContentPurifier()->purify(
599  $string_with_replaced_gaps
600  );
601  $cleaned_text_with_gaps = preg_replace_callback('/######GAP######/', function ($match) use (&$gaps) {
602  return array_shift($gaps[0]);
603  }, $cleaned_text);
604 
606  || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
607  $cleaned_text_with_gaps = nl2br($cleaned_text_with_gaps);
608  }
609 
610  return $this->prepareTextareaOutput($cleaned_text_with_gaps, true);
611  }
612 
620  public function getStartTag(): string
621  {
622  return $this->start_tag;
623  }
624 
632  public function setStartTag($start_tag = "[gap]"): void
633  {
634  $this->start_tag = $start_tag;
635  }
636 
644  public function getEndTag(): string
645  {
646  return $this->end_tag;
647  }
648 
656  public function setEndTag($end_tag = "[/gap]"): void
657  {
658  $this->end_tag = $end_tag;
659  }
660 
664  public function getFeedbackMode(): string
665  {
666  return $this->feedbackMode;
667  }
668 
672  public function setFeedbackMode($feedbackMode): void
673  {
674  $this->feedbackMode = $feedbackMode;
675  }
676 
683  public function createGapsFromQuestiontext(): void
684  {
685  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
686  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
687  $search_pattern = "|\[gap\](.*?)\[/gap\]|i";
688  preg_match_all($search_pattern, $this->getClozeText(), $found);
689  $this->gaps = [];
690  if (count($found[0])) {
691  foreach ($found[1] as $gap_index => $answers) {
692  // create text gaps by default
693  $gap = new assClozeGap(CLOZE_TEXT);
694  $textparams = preg_split("/(?<!\\\\),/", $answers);
695  foreach ($textparams as $key => $value) {
696  $answer = new assAnswerCloze($value, 0, $key);
697  $gap->addItem($answer);
698  }
699  $this->gaps[$gap_index] = $gap;
700  }
701  }
702  }
703 
709  public function setGapType($gap_index, $gap_type): void
710  {
711  if (array_key_exists($gap_index, $this->gaps)) {
712  $this->gaps[$gap_index]->setType($gap_type);
713  }
714  }
715 
725  public function setGapShuffle($gap_index = 0, $shuffle = 1): void
726  {
727  if (array_key_exists($gap_index, $this->gaps)) {
728  $this->gaps[$gap_index]->setShuffle($shuffle);
729  }
730  }
731 
738  public function clearGapAnswers(): void
739  {
740  foreach ($this->gaps as $gap_index => $gap) {
741  $this->gaps[$gap_index]->clearItems();
742  }
743  }
744 
752  public function getGapCount(): int
753  {
754  if (is_array($this->gaps)) {
755  return count($this->gaps);
756  } else {
757  return 0;
758  }
759  }
760 
771  public function addGapAnswer($gap_index, $order, $answer): void
772  {
773  if (array_key_exists($gap_index, $this->gaps)) {
774  if ($this->gaps[$gap_index]->getType() == CLOZE_NUMERIC) {
775  // only allow notation with "." for real numbers
776  $answer = str_replace(",", ".", $answer);
777  }
778  $this->gaps[$gap_index]->addItem(new assAnswerCloze(trim($answer), 0, $order));
779  }
780  }
781 
788  public function getGap($gap_index = 0)
789  {
790  if (array_key_exists($gap_index, $this->gaps)) {
791  return $this->gaps[$gap_index];
792  } else {
793  return null;
794  }
795  }
796 
797  public function setGapSize($gap_index, $size): void
798  {
799  if (array_key_exists($gap_index, $this->gaps)) {
800  $this->gaps[$gap_index]->setGapSize((int) $size);
801  }
802  }
803 
814  public function setGapAnswerPoints($gap_index, $order, $points): void
815  {
816  if (array_key_exists($gap_index, $this->gaps)) {
817  $this->gaps[$gap_index]->setItemPoints($order, $points);
818  }
819  }
820 
829  public function addGapText($gap_index): void
830  {
831  if (array_key_exists($gap_index, $this->gaps)) {
832  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
833  $answer = new assAnswerCloze(
834  "",
835  0,
836  $this->gaps[$gap_index]->getItemCount()
837  );
838  $this->gaps[$gap_index]->addItem($answer);
839  }
840  }
841 
850  public function addGapAtIndex($gap, $index): void
851  {
852  $this->gaps[$index] = $gap;
853  }
854 
865  public function setGapAnswerLowerBound($gap_index, $order, $bound): void
866  {
867  if (array_key_exists($gap_index, $this->gaps)) {
868  $this->gaps[$gap_index]->setItemLowerBound($order, $bound);
869  }
870  }
871 
882  public function setGapAnswerUpperBound($gap_index, $order, $bound): void
883  {
884  if (array_key_exists($gap_index, $this->gaps)) {
885  $this->gaps[$gap_index]->setItemUpperBound($order, $bound);
886  }
887  }
888 
895  public function getMaximumPoints(): float
896  {
897  $assClozeGapCombinationObj = new assClozeGapCombination();
898  $points = 0;
899  $gaps_used_in_combination = [];
900  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
901  $points = $assClozeGapCombinationObj->getMaxPointsForCombination($this->getId());
902  $gaps_used_in_combination = $assClozeGapCombinationObj->getGapsWhichAreUsedInCombination($this->getId());
903  }
904  foreach ($this->gaps as $gap_index => $gap) {
905  if (!array_key_exists($gap_index, $gaps_used_in_combination)) {
906  if ($gap->getType() == CLOZE_TEXT) {
907  $gap_max_points = 0;
908  foreach ($gap->getItems($this->getShuffler()) as $item) {
909  if ($item->getPoints() > $gap_max_points) {
910  $gap_max_points = $item->getPoints();
911  }
912  }
913  $points += $gap_max_points;
914  } elseif ($gap->getType() == CLOZE_SELECT) {
915  $srpoints = 0;
916  foreach ($gap->getItems($this->getShuffler()) as $item) {
917  if ($item->getPoints() > $srpoints) {
918  $srpoints = $item->getPoints();
919  }
920  }
921  $points += $srpoints;
922  } elseif ($gap->getType() == CLOZE_NUMERIC) {
923  $numpoints = 0;
924  foreach ($gap->getItems($this->getShuffler()) as $item) {
925  if ($item->getPoints() > $numpoints) {
926  $numpoints = $item->getPoints();
927  }
928  }
929  $points += $numpoints;
930  }
931  }
932  }
933 
934  return $points;
935  }
936 
942  public function duplicate(bool $for_test = true, string $title = "", string $author = "", string $owner = "", $testObjId = null): int
943  {
944  if ($this->id <= 0) {
945  // The question has not been saved. It cannot be duplicated
946  return -1;
947  }
948  // duplicate the question in database
949  $this_id = $this->getId();
950  $thisObjId = $this->getObjId();
951 
952  $clone = $this;
953  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
955  $clone->id = -1;
956 
957  if ((int) $testObjId > 0) {
958  $clone->setObjId($testObjId);
959  }
960 
961  if ($title) {
962  $clone->setTitle($title);
963  }
964  if ($author) {
965  $clone->setAuthor($author);
966  }
967  if ($owner) {
968  $clone->setOwner($owner);
969  }
970  if ($for_test) {
971  $clone->saveToDb($original_id);
972  } else {
973  $clone->saveToDb();
974  }
975  if ($this->gap_combinations_exists) {
976  $this->copyGapCombination($this_id, $clone->getId());
977  }
978  if ($for_test) {
979  $clone->saveToDb($original_id);
980  } else {
981  $clone->saveToDb();
982  }
983  // copy question page content
984  $clone->copyPageOfQuestion($this_id);
985  // copy XHTML media objects
986  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
987 
988  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
989 
990  return $clone->getId();
991  }
992 
998  public function copyObject($target_questionpool_id, $title = ""): int
999  {
1000  if ($this->getId() <= 0) {
1001  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
1002  }
1003 
1004  $thisId = $this->getId();
1005  $thisObjId = $this->getObjId();
1006 
1007  $clone = $this;
1008  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
1010  $clone->id = -1;
1011  $clone->setObjId($target_questionpool_id);
1012  if ($title) {
1013  $clone->setTitle($title);
1014  }
1015 
1016  $clone->saveToDb();
1017 
1018  if ($this->gap_combinations_exists) {
1019  $this->copyGapCombination($original_id, $clone->getId());
1020  $clone->saveToDb();
1021  }
1022 
1023  // copy question page content
1024  $clone->copyPageOfQuestion($original_id);
1025  // copy XHTML media objects
1026  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
1027 
1028  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
1029 
1030  return $clone->getId();
1031  }
1032 
1033  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
1034  {
1035  if ($this->getId() <= 0) {
1036  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
1037  }
1038 
1039  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
1040 
1041  $sourceQuestionId = $this->id;
1042  $sourceParentId = $this->getObjId();
1043 
1044  // duplicate the question in database
1045  $clone = $this;
1046  $clone->id = -1;
1047 
1048  $clone->setObjId($targetParentId);
1049 
1050  if ($targetQuestionTitle) {
1051  $clone->setTitle($targetQuestionTitle);
1052  }
1053 
1054  $clone->saveToDb();
1055 
1056  if ($this->gap_combinations_exists) {
1057  $this->copyGapCombination($sourceQuestionId, $clone->getId());
1058  $clone->saveToDb();
1059  }
1060  // copy question page content
1061  $clone->copyPageOfQuestion($sourceQuestionId);
1062  // copy XHTML media objects
1063  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
1064 
1065  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
1066 
1067  return $clone->id;
1068  }
1069 
1070  public function copyGapCombination($orgID, $newID): void
1071  {
1072  $assClozeGapCombinationObj = new assClozeGapCombination();
1073  $array = $assClozeGapCombinationObj->loadFromDb($orgID);
1074  $assClozeGapCombinationObj->importGapCombinationToDb($newID, $array);
1075  }
1076 
1082  public function updateClozeTextFromGaps(): void
1083  {
1084  $output = $this->getClozeText();
1085  foreach ($this->getGaps() as $gap_index => $gap) {
1086  $answers = [];
1087  foreach ($gap->getItemsRaw() as $item) {
1088  array_push($answers, str_replace([',', '['], ["\\,", '[&hairsp;'], $item->getAnswerText()));
1089  }
1090  // fau: fixGapReplace - use replace function
1091  $output = $this->replaceFirstGap($output, "[_gap]" . $this->prepareTextareaOutput(join(",", $answers), true) . "[/_gap]");
1092  // fau.
1093  }
1094  $output = str_replace("_gap]", "gap]", $output);
1095  $this->cloze_text = $output;
1096  }
1097 
1107  public function deleteAnswerText($gap_index, $answer_index): void
1108  {
1109  if (array_key_exists($gap_index, $this->gaps)) {
1110  if ($this->gaps[$gap_index]->getItemCount() == 1) {
1111  // this is the last answer text => remove the gap
1112  $this->deleteGap($gap_index);
1113  } else {
1114  // remove the answer text
1115  $this->gaps[$gap_index]->deleteItem($answer_index);
1116  $this->updateClozeTextFromGaps();
1117  }
1118  }
1119  }
1120 
1129  public function deleteGap($gap_index): void
1130  {
1131  if (array_key_exists($gap_index, $this->gaps)) {
1132  $output = $this->getClozeText();
1133  foreach ($this->getGaps() as $replace_gap_index => $gap) {
1134  $answers = [];
1135  foreach ($gap->getItemsRaw() as $item) {
1136  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
1137  }
1138  if ($replace_gap_index == $gap_index) {
1139  // fau: fixGapReplace - use replace function
1140  $output = $this->replaceFirstGap($output, '');
1141  // fau.
1142  } else {
1143  // fau: fixGapReplace - use replace function
1144  $output = $this->replaceFirstGap($output, "[_gap]" . join(",", $answers) . "[/_gap]");
1145  // fau.
1146  }
1147  }
1148  $output = str_replace("_gap]", "gap]", $output);
1149  $this->cloze_text = $output;
1150  unset($this->gaps[$gap_index]);
1151  $this->gaps = array_values($this->gaps);
1152  }
1153  }
1154 
1164  public function getTextgapPoints($a_original, $a_entered, $max_points): float
1165  {
1166  include_once "./Services/Utilities/classes/class.ilStr.php";
1167  global $DIC;
1168  $refinery = $DIC->refinery();
1169  $result = 0;
1170  $gaprating = $this->getTextgapRating();
1171 
1172  switch ($gaprating) {
1174  if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) {
1175  $result = $max_points;
1176  }
1177  break;
1179  if (strcmp($a_original, $a_entered) == 0) {
1180  $result = $max_points;
1181  }
1182  break;
1184  $transformation = $refinery->string()->levenshtein()->standard($a_original, 1);
1185  break;
1187  $transformation = $refinery->string()->levenshtein()->standard($a_original, 2);
1188  break;
1190  $transformation = $refinery->string()->levenshtein()->standard($a_original, 3);
1191  break;
1193  $transformation = $refinery->string()->levenshtein()->standard($a_original, 4);
1194  break;
1196  $transformation = $refinery->string()->levenshtein()->standard($a_original, 5);
1197  break;
1198  }
1199 
1200  // run answers against Levenshtein2 methods
1201  if (isset($transformation) && $transformation->transform($a_entered) >= 0) {
1202  $result = $max_points;
1203  }
1204  return $result;
1205  }
1206 
1207 
1217  public function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound): float
1218  {
1219  include_once "./Services/Math/classes/class.EvalMath.php";
1220  $eval = new EvalMath();
1221  $eval->suppress_errors = true;
1222  $result = 0.0;
1223 
1224  if ($eval->e($a_entered) === false) {
1225  return 0.0;
1226  } elseif (($eval->e($lowerBound) !== false) && ($eval->e($upperBound) !== false)) {
1227  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
1228  $result = $max_points;
1229  }
1230  } elseif ($eval->e($lowerBound) !== false) {
1231  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) {
1232  $result = $max_points;
1233  }
1234  } elseif ($eval->e($upperBound) !== false) {
1235  if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
1236  $result = $max_points;
1237  }
1238  } elseif ($eval->e($a_entered) == $eval->e($a_original)) {
1239  $result = $max_points;
1240  }
1241  return $result;
1242  }
1243 
1248  public function checkForValidFormula($value): int
1249  {
1250  return preg_match("/^-?(\\d*)(,|\\.|\\/){0,1}(\\d*)$/", $value, $matches);
1251  }
1261  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false)
1262  {
1263  $ilDB = $this->db;
1264 
1265  if (is_null($pass)) {
1266  $pass = $this->getSolutionMaxPass($active_id);
1267  }
1268 
1269  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
1270  $user_result = [];
1271  while ($data = $ilDB->fetchAssoc($result)) {
1272  if (strcmp($data["value2"], "") != 0) {
1273  $user_result[$data["value1"]] = array(
1274  "gap_id" => $data["value1"],
1275  "value" => $data["value2"]
1276  );
1277  }
1278  }
1279 
1280  ksort($user_result); // this is required when identical scoring for same solutions is disabled
1281 
1282  if ($returndetails) {
1283  $detailed = [];
1284  $this->calculateReachedPointsForSolution($user_result, $detailed);
1285  return $detailed;
1286  }
1287 
1288  return $this->calculateReachedPointsForSolution($user_result);
1289  }
1290 
1291  protected function isValidNumericSubmitValue($submittedValue): bool
1292  {
1293  if (is_numeric($submittedValue)) {
1294  return true;
1295  }
1296 
1297  if (preg_match('/^[-+]{0,1}\d+\/\d+$/', $submittedValue)) {
1298  return true;
1299  }
1300 
1301  return false;
1302  }
1303 
1304  public function validateSolutionSubmit(): bool
1305  {
1306  foreach ($this->getSolutionSubmitValidation() as $gapIndex => $value) {
1307  $gap = $this->getGap($gapIndex);
1308 
1309  if ($gap->getType() != CLOZE_NUMERIC) {
1310  continue;
1311  }
1312 
1313  if (strlen($value) && !$this->isValidNumericSubmitValue($value)) {
1314  $this->tpl->setOnScreenMessage('failure', $this->lng->txt("err_no_numeric_value"), true);
1315  return false;
1316  }
1317  }
1318 
1319  return true;
1320  }
1321 
1322  public function fetchSolutionSubmit($submit): array
1323  {
1324  $solutionSubmit = [];
1325  $post_wrapper = $this->dic->http()->wrapper()->post();
1326  foreach ($this->getGaps() as $index => $gap) {
1327  if (!$post_wrapper->has("gap_$index")) {
1328  continue;
1329  }
1330  $value = trim($post_wrapper->retrieve(
1331  "gap_$index",
1332  $this->dic->refinery()->kindlyTo()->string()
1333  ));
1334  if ($value === '') {
1335  continue;
1336  }
1337 
1338  if (!(($gap->getType() === (int) CLOZE_SELECT) && ($value === -1))) {
1339  if (
1340  $gap->getType() === (int) CLOZE_NUMERIC
1341  && !is_numeric(str_replace(",", ".", $value))
1342  ) {
1343  $value = null;
1344  } elseif ($gap->getType() === (int) CLOZE_NUMERIC) {
1345  $value = str_replace(",", ".", $value);
1346  }
1347  $solutionSubmit[$index] = $value;
1348  }
1349  }
1350 
1351  return $solutionSubmit;
1352  }
1353 
1354  public function getSolutionSubmitValidation(): array
1355  {
1356  $submit = $_POST;
1357  $solutionSubmit = [];
1358 
1359  foreach ($submit as $key => $value) {
1360  if (preg_match("/^gap_(\d+)/", $key, $matches)) {
1361  if ($value !== null && $value !== '') {
1362  $gap = $this->getGap($matches[1]);
1363  if (is_object($gap)) {
1364  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1))) {
1365  if ($gap->getType() == CLOZE_NUMERIC) {
1366  $value = str_replace(",", ".", $value);
1367  }
1368  $solutionSubmit[trim($matches[1])] = $value;
1369  }
1370  }
1371  }
1372  }
1373  }
1374 
1375  return $solutionSubmit;
1376  }
1377 
1378  public function getSolutionSubmit(): array
1379  {
1380  return $this->fetchSolutionSubmit($_POST);
1381  }
1382 
1391  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
1392  {
1393  if (is_null($pass)) {
1394  include_once "./Modules/Test/classes/class.ilObjTest.php";
1395  $pass = ilObjTest::_getPass($active_id);
1396  }
1397 
1398  $entered_values = 0;
1399 
1400  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
1401  $this->removeCurrentSolution($active_id, $pass, $authorized);
1402 
1403  foreach ($this->getSolutionSubmit() as $key => $value) {
1404  if ($value !== null && $value !== '') {
1405  $gap = $this->getGap(trim(ilUtil::stripSlashes($key)));
1406  if (is_object($gap)) {
1407  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1))) {
1408  $this->saveCurrentSolution($active_id, $pass, $key, $value, $authorized);
1409  $entered_values++;
1410  }
1411  }
1412  }
1413  }
1414  });
1415 
1416  if ($entered_values) {
1417  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1419  assQuestion::logAction($this->lng->txtlng(
1420  "assessment",
1421  "log_user_entered_values",
1423  ), $active_id, $this->getId());
1424  }
1425  } else {
1426  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1428  assQuestion::logAction($this->lng->txtlng(
1429  "assessment",
1430  "log_user_not_entered_values",
1432  ), $active_id, $this->getId());
1433  }
1434  }
1435 
1436  return true;
1437  }
1438 
1445  public function getQuestionType(): string
1446  {
1447  return "assClozeTest";
1448  }
1449 
1457  public function getTextgapRating(): string
1458  {
1459  return $this->textgap_rating;
1460  }
1461 
1469  public function setTextgapRating($a_textgap_rating): void
1470  {
1471  switch ($a_textgap_rating) {
1479  $this->textgap_rating = $a_textgap_rating;
1480  break;
1481  default:
1482  $this->textgap_rating = TEXTGAP_RATING_CASEINSENSITIVE;
1483  break;
1484  }
1485  }
1486 
1494  public function getIdenticalScoring()
1495  {
1496  return ($this->identical_scoring) ? 1 : 0;
1497  }
1498 
1506  public function setIdenticalScoring($a_identical_scoring): void
1507  {
1508  $this->identical_scoring = ($a_identical_scoring) ? 1 : 0;
1509  }
1510 
1517  public function getAdditionalTableName(): string
1518  {
1519  return "qpl_qst_cloze";
1520  }
1521 
1522  public function getAnswerTableName(): array
1523  {
1524  return array("qpl_a_cloze",'qpl_a_cloze_combi_res');
1525  }
1526 
1533  public function setFixedTextLength($a_text_len): void
1534  {
1535  $this->fixedTextLength = $a_text_len;
1536  }
1537 
1544  public function getFixedTextLength()
1545  {
1546  return $this->fixedTextLength;
1547  }
1548 
1557  public function getMaximumGapPoints($gap_index)
1558  {
1559  $points = 0;
1560  $gap_max_points = 0;
1561  if (array_key_exists($gap_index, $this->gaps)) {
1562  $gap = &$this->gaps[$gap_index];
1563  foreach ($gap->getItems($this->getShuffler()) as $answer) {
1564  if ($answer->getPoints() > $gap_max_points) {
1565  $gap_max_points = $answer->getPoints();
1566  }
1567  }
1568  $points += $gap_max_points;
1569  }
1570  return $points;
1571  }
1572 
1577  public function getRTETextWithMediaObjects(): string
1578  {
1579  return parent::getRTETextWithMediaObjects() . $this->getClozeText();
1580  }
1581  public function getGapCombinationsExists(): bool
1582  {
1584  }
1585 
1586  public function getGapCombinations(): array
1587  {
1588  return $this->gap_combinations;
1589  }
1590 
1591  public function setGapCombinationsExists($value): void
1592  {
1593  $this->gap_combinations_exists = $value;
1594  }
1595 
1596  public function setGapCombinations($value): void
1597  {
1598  $this->gap_combinations = $value;
1599  }
1600 
1604  public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass): int
1605  {
1606  parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
1607 
1608  $solution = $this->getSolutionValues($active_id, $pass);
1609  $i = 1;
1610  foreach ($this->getGaps() as $gap_index => $gap) {
1611  $worksheet->setCell($startrow + $i, 0, $this->lng->txt("gap") . " $i");
1612  $worksheet->setBold($worksheet->getColumnCoord(0) . ($startrow + $i));
1613  $checked = false;
1614  foreach ($solution as $solutionvalue) {
1615  if ($gap_index == $solutionvalue["value1"]) {
1616  $string_escaping_org_value = $worksheet->getStringEscaping();
1617  try {
1618  $worksheet->setStringEscaping(false);
1619 
1620  switch ($gap->getType()) {
1621  case CLOZE_SELECT:
1622  $worksheet->setCell($startrow + $i, 2, $gap->getItem($solutionvalue["value2"])->getAnswertext());
1623  break;
1624  case CLOZE_NUMERIC:
1625  case CLOZE_TEXT:
1626  $worksheet->setCell($startrow + $i, 2, $solutionvalue["value2"]);
1627  break;
1628  }
1629  } finally {
1630  $worksheet->setStringEscaping($string_escaping_org_value);
1631  }
1632  }
1633  }
1634  $i++;
1635  }
1636 
1637  return $startrow + $i + 1;
1638  }
1639 
1644  {
1645  // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1646  //$this->setClozeText( $migrator->migrateToLmContent($this->getClozeText()) );
1647  $this->cloze_text = $migrator->migrateToLmContent($this->getClozeText());
1648  // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1649  }
1650 
1654  public function toJSON(): string
1655  {
1656  include_once("./Services/RTE/classes/class.ilRTE.php");
1657  $result = [];
1658  $result['id'] = $this->getId();
1659  $result['type'] = (string) $this->getQuestionType();
1660  $result['title'] = $this->getTitleForHTMLOutput();
1661  $result['question'] = $this->formatSAQuestion($this->getQuestion());
1662  $result['clozetext'] = $this->formatSAQuestion($this->getClozeText());
1663  $result['nr_of_tries'] = $this->getNrOfTries();
1664  $result['shuffle'] = $this->getShuffle();
1665  $result['feedback'] = array(
1666  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1667  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1668  );
1669 
1670  $gaps = [];
1671  foreach ($this->getGaps() as $key => $gap) {
1672  $items = [];
1673  foreach ($gap->getItems($this->getShuffler()) as $item) {
1674  $jitem = [];
1675  $jitem['points'] = $item->getPoints();
1676  $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1677  $jitem['order'] = $item->getOrder();
1678  if ($gap->getType() == CLOZE_NUMERIC) {
1679  $jitem['lowerbound'] = $item->getLowerBound();
1680  $jitem['upperbound'] = $item->getUpperBound();
1681  } else {
1682  $jitem['value'] = trim($jitem['value']);
1683  }
1684  array_push($items, $jitem);
1685  }
1686 
1687  if ($gap->getGapSize() && ($gap->getType() == CLOZE_TEXT || $gap->getType() == CLOZE_NUMERIC)) {
1688  $jgap['size'] = $gap->getGapSize();
1689  }
1690 
1691  $jgap['shuffle'] = $gap->getShuffle();
1692  $jgap['type'] = $gap->getType();
1693  $jgap['item'] = $items;
1694 
1695  array_push($gaps, $jgap);
1696  }
1697  $result['gaps'] = $gaps;
1698  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1699  $result['mobs'] = $mobs;
1700  return json_encode($result);
1701  }
1702 
1711  public function getOperators($expression): array
1712  {
1714  }
1715 
1720  public function getExpressionTypes(): array
1721  {
1722  return [
1728  ];
1729  }
1730 
1739  public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
1740  {
1742  global $DIC;
1743  $ilDB = $DIC['ilDB'];
1744  $result = new ilUserQuestionResult($this, $active_id, $pass);
1745 
1746  $maxStep = $this->lookupMaxStep($active_id, $pass);
1747 
1748  if ($maxStep !== null) {
1749  $data = $ilDB->queryF(
1750  "
1751  SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1752  FROM tst_solutions sol
1753  INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1754  WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s AND sol.step = %s
1755  GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1756  ",
1757  array("integer", "integer", "integer","integer"),
1758  array($active_id, $pass, $this->getId(), $maxStep)
1759  );
1760  } else {
1761  $data = $ilDB->queryF(
1762  "
1763  SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1764  FROM tst_solutions sol
1765  INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1766  WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s
1767  GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1768  ",
1769  array("integer", "integer", "integer"),
1770  array($active_id, $pass, $this->getId())
1771  );
1772  }
1773 
1774  while ($row = $ilDB->fetchAssoc($data)) {
1775  if ($row["cloze_type"] == 1) {
1776  $row["value2"]++;
1777  }
1778  $result->addKeyValue($row["val"], $row["value2"]);
1779  }
1780 
1781  $points = $this->calculateReachedPoints($active_id, $pass);
1782  $max_points = $this->getMaximumPoints();
1783 
1784  $result->setReachedPercentage(($points / $max_points) * 100);
1785 
1786  return $result;
1787  }
1788 
1797  public function getAvailableAnswerOptions($index = null)
1798  {
1799  if ($index !== null) {
1800  return $this->getGap($index);
1801  } else {
1802  return $this->getGaps();
1803  }
1804  }
1805 
1806  public function calculateCombinationResult($user_result): array
1807  {
1808  $points = 0;
1809 
1810  $assClozeGapCombinationObj = new assClozeGapCombination();
1811  $gap_used_in_combination = [];
1812  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1813  $combinations_for_question = $assClozeGapCombinationObj->getCleanCombinationArray($this->getId());
1814  $gap_answers = [];
1815 
1816  foreach ($user_result as $user_result_build_list) {
1817  if (is_array($user_result_build_list)) {
1818  $gap_answers[$user_result_build_list['gap_id']] = $user_result_build_list['value'];
1819  }
1820  }
1821 
1822  foreach ($combinations_for_question as $combination) {
1823  foreach ($combination as $row_key => $row_answers) {
1824  $combination_fulfilled = true;
1825  $points_for_combination = $row_answers['points'];
1826  foreach ($row_answers as $gap_key => $combination_gap_answer) {
1827  if ($gap_key !== 'points') {
1828  $gap_used_in_combination[$gap_key] = $gap_key;
1829  }
1830  if ($combination_fulfilled && array_key_exists($gap_key, $gap_answers)) {
1831  switch ($combination_gap_answer['type']) {
1832  case CLOZE_TEXT:
1833  $is_text_gap_correct = $this->getTextgapPoints($gap_answers[$gap_key], $combination_gap_answer['answer'], 1);
1834  if ($is_text_gap_correct != 1) {
1835  $combination_fulfilled = false;
1836  }
1837  break;
1838  case CLOZE_SELECT:
1839  $answer = $this->gaps[$gap_key]->getItem($gap_answers[$gap_key]);
1840  $answertext = '';
1841  if ($answer !== null) {
1842  $answertext = $answer->getAnswertext();
1843  }
1844 
1845  if ($answertext != $combination_gap_answer['answer']) {
1846  $combination_fulfilled = false;
1847  }
1848  break;
1849  case CLOZE_NUMERIC:
1850  $answer = $this->gaps[$gap_key]->getItem(0);
1851  if ($combination_gap_answer['answer'] != 'out_of_bound') {
1852  $is_numeric_gap_correct = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1853  if ($is_numeric_gap_correct != 1) {
1854  $combination_fulfilled = false;
1855  }
1856  } else {
1857  $wrong_is_the_new_right = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1858  if ($wrong_is_the_new_right == 1) {
1859  $combination_fulfilled = false;
1860  }
1861  }
1862  break;
1863  }
1864  } else {
1865  if ($gap_key !== 'points') {
1866  $combination_fulfilled = false;
1867  }
1868  }
1869  }
1870  if ($combination_fulfilled) {
1871  $points += $points_for_combination;
1872  }
1873  }
1874  }
1875  }
1876  return array($points, $gap_used_in_combination);
1877  }
1882  protected function calculateReachedPointsForSolution($user_result, &$detailed = null): float
1883  {
1884  if ($detailed === null) {
1885  $detailed = [];
1886  }
1887 
1888  $points = 0;
1889 
1890  $assClozeGapCombinationObj = new assClozeGapCombination();
1891  $combinations[1] = [];
1892  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1893  $combinations = $this->calculateCombinationResult($user_result);
1894  $points = $combinations[0];
1895  }
1896  $counter = 0;
1897  $solution_values_text = []; // for identical scoring checks
1898  $solution_values_select = []; // for identical scoring checks
1899  $solution_values_numeric = []; // for identical scoring checks
1900  foreach ($user_result as $gap_id => $value) {
1901  if (is_string($value)) {
1902  $value = array("value" => $value);
1903  }
1904 
1905  if (array_key_exists($gap_id, $this->gaps) && !array_key_exists($gap_id, $combinations[1])) {
1906  switch ($this->gaps[$gap_id]->getType()) {
1907  case CLOZE_TEXT:
1908  $gappoints = 0;
1909  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1910  $answer = $this->gaps[$gap_id]->getItem($order);
1911  $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1912  if ($gotpoints > $gappoints) {
1913  $gappoints = $gotpoints;
1914  }
1915  }
1916  if (!$this->getIdenticalScoring()) {
1917  // check if the same solution text was already entered
1918  if ((in_array($value["value"], $solution_values_text)) && ($gappoints > 0)) {
1919  $gappoints = 0;
1920  }
1921  }
1922  $points += $gappoints;
1923  $detailed[$gap_id] = array("points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0) ? true : false);
1924  array_push($solution_values_text, $value["value"]);
1925  break;
1926  case CLOZE_NUMERIC:
1927  $gappoints = 0;
1928  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1929  $answer = $this->gaps[$gap_id]->getItem($order);
1930  $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1931  if ($gotpoints > $gappoints) {
1932  $gappoints = $gotpoints;
1933  }
1934  }
1935  if (!$this->getIdenticalScoring()) {
1936  // check if the same solution value was already entered
1937  include_once "./Services/Math/classes/class.EvalMath.php";
1938  $eval = new EvalMath();
1939  $eval->suppress_errors = true;
1940  $found_value = false;
1941  foreach ($solution_values_numeric as $solval) {
1942  if ($eval->e($solval) == $eval->e($value["value"])) {
1943  $found_value = true;
1944  }
1945  }
1946  if ($found_value && ($gappoints > 0)) {
1947  $gappoints = 0;
1948  }
1949  }
1950  $points += $gappoints;
1951  $detailed[$gap_id] = array("points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0) ? true : false);
1952  array_push($solution_values_numeric, $value["value"]);
1953  break;
1954  case CLOZE_SELECT:
1955  if ($value["value"] >= 0) {
1956  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1957  $answer = $this->gaps[$gap_id]->getItem($order);
1958  if ($value["value"] == $answer->getOrder()) {
1959  $answerpoints = $answer->getPoints();
1960  if (!$this->getIdenticalScoring()) {
1961  // check if the same solution value was already entered
1962  if ((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0)) {
1963  $answerpoints = 0;
1964  }
1965  }
1966  $points += $answerpoints;
1967  $detailed[$gap_id] = array("points" => $answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? true : false, "positive" => ($answerpoints > 0) ? true : false);
1968  array_push($solution_values_select, $answer->getAnswertext());
1969  }
1970  }
1971  }
1972  break;
1973  }
1974  }
1975  }
1976 
1977  return $points;
1978  }
1979 
1981  {
1982  $participant_session = $preview_session->getParticipantsSolution();
1983 
1984  if (!is_array($participant_session)) {
1985  return 0;
1986  }
1987 
1988  $user_solution = [];
1989 
1990  foreach ($participant_session as $key => $val) {
1991  $user_solution[$key] = array('gap_id' => $key, 'value' => $val);
1992  }
1993 
1994  $reached_points = $this->calculateReachedPointsForSolution($user_solution);
1995  $reached_points = $this->deductHintPointsFromReachedPoints($preview_session, $reached_points);
1996 
1997  return $this->ensureNonNegativePoints($reached_points);
1998  }
1999 
2000  public function fetchAnswerValueForGap($userSolution, $gapIndex): string
2001  {
2002  $answerValue = '';
2003 
2004  foreach ($userSolution as $value1 => $value2) {
2005  if ($value1 == $gapIndex) {
2006  $answerValue = $value2;
2007  break;
2008  }
2009  }
2010 
2011  return $answerValue;
2012  }
2013 
2014  public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
2015  {
2016  $gap = $this->getGap($qIndex);
2017 
2018  if ($gap->getType() != CLOZE_TEXT) {
2019  return false;
2020  }
2021 
2022  foreach ($gap->getItems($this->randomGroup->dontShuffle()) as $item) {
2023  if ($item->getAnswertext() === $answerOptionValue) {
2024  return false;
2025  }
2026  }
2027 
2028  return true;
2029  }
2030 
2031  public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
2032  {
2033  $gap = $this->getGap($qIndex); /* @var assClozeGap $gap */
2034 
2035  $item = new assAnswerCloze($answerOptionValue, $points);
2036  $item->setOrder($gap->getItemCount());
2037 
2038  $gap->addItem($item);
2039  }
2040 
2041  public function savePartial(): bool
2042  {
2043  return true;
2044  }
2045 }
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.
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $testObjId=null)
Duplicates an assClozeTest.
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.
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)
$mobs
Definition: imgupload.php:70
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...
static _getOriginalId(int $question_id)
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...
$index
Definition: metadata.php:145
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
const TEXTGAP_RATING_LEVENSHTEIN3
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
$query
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.
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
{}
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.
__construct(Container $dic, ilPlugin $plugin)
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)
prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
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="")
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
$i
Definition: metadata.php:41
const TEXTGAP_RATING_CASEINSENSITIVE
setQuestion(string $question="")