ILIAS  release_8 Revision v8.24
class.assClozeTest.php
Go to the documentation of this file.
1<?php
2
19use ILIAS\Refinery\Random\Group as RandomGroup;
20
21require_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
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']);
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
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 {
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 {
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}
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Class for cloze question gaps.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
clearGapAnswers()
Removes all answers from the gaps.
getEndTag()
Returns the end tag of a cloze gap.
addGapAnswer($gap_index, $order, $answer)
Sets the answer text of a gap with a given index.
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
isComplete()
Returns TRUE, if a cloze test is complete for use.
setGapShuffle($gap_index=0, $shuffle=1)
Sets the shuffle state of a gap with a given index.
setGapSize($gap_index, $size)
getClozeText()
Returns the cloze text.
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...
isValidNumericSubmitValue($submittedValue)
setGapAnswerPoints($gap_index, $order, $points)
Sets the points of a gap with a given index and an answer with a given order.
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
setFeedbackMode($feedbackMode)
copyObject($target_questionpool_id, $title="")
Copies an assClozeTest object.
setClozeTextValue($cloze_text="")
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
addGapAtIndex($gap, $index)
Adds a ClozeGap object at a given index.
setTextgapRating($a_textgap_rating)
Sets the rating option for text gaps.
getAdditionalTableName()
Returns the name of the additional question data table in the database.
setGapAnswerUpperBound($gap_index, $order, $bound)
Sets the upper bound of a gap with a given index and an answer with a given order.
createGapsFromQuestiontext()
Create gap entries by parsing the question text.
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
saveWorkingData($active_id, $pass=null, $authorized=true)
Saves the learners input of the question to the database.
RandomGroup $randomGroup
updateClozeTextFromGaps()
Updates the gap parameters in the cloze text from the form input.
flushGaps()
Deletes all gaps without changing the cloze text.
getStartTag()
Returns the start tag of a cloze gap.
saveAnswerSpecificDataToDb()
Save all gaps to the database.
getFixedTextLength()
Gets the fixed text length for all text fields in the cloze question.
ilAssQuestionFeedback $feedbackOBJ
addGapText($gap_index)
Adds a new answer text value to a text gap with a given index.
setIdenticalScoring($a_identical_scoring)
Sets the identical scoring option for cloze questions.
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
saveToDb($original_id="")
Saves a assClozeTest object to a database.
getGaps()
Returns the array of gaps.
getOperators($expression)
Get all available operations for a specific question.
__construct( $title="", $comment="", $author="", $owner=-1, $question="")
assClozeTest constructor
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
{}
getQuestionType()
Returns the question type of the question.
checkForValidFormula($value)
getTextgapRating()
Returns the rating option for text gaps.
deleteGap($gap_index)
Deletes a gap with a given index.
getGap($gap_index=0)
Returns the gap at a given index.
setGapType($gap_index, $gap_type)
Set the type of a gap with a given index.
copyGapCombination($orgID, $newID)
calculateReachedPointsForSolution($user_result, &$detailed=null)
getExpressionTypes()
Get all available expression types for a specific question.
saveClozeTextGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
getGapCount()
Returns the number of gaps.
setGapCombinationsExists($value)
saveClozeGapItemsToDb($gap, $key)
Save all items belonging to one cloze gap to the db.
setGapAnswerLowerBound($gap_index, $order, $bound)
Sets the lower bound of a gap with a given index and an answer with a given order.
setEndTag($end_tag="[/gap]")
Sets the end tag of a cloze gap.
fetchAnswerValueForGap($userSolution, $gapIndex)
getMaximumGapPoints($gap_index)
Returns the maximum points for a gap.
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
saveAdditionalQuestionDataToDb()
Saves the data for the additional data table.
deleteAnswerText($gap_index, $answer_index)
Deletes the answer text of a gap with a given index and an answer with a given order.
saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
fetchSolutionSubmit($submit)
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $testObjId=null)
Duplicates an assClozeTest.
toJSON()
Returns a JSON representation of the question.
loadFromDb($question_id)
Loads a assClozeTest object from a database.
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
setFixedTextLength($a_text_len)
Sets a fixed text length for all text fields in the cloze question.
calculateCombinationResult($user_result)
setClozeText($cloze_text="")
Evaluates the text gap solutions from the cloze text.
saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
replaceFirstGap($gaptext, $content)
Replace the first gap in a string without treating backreferences.
setStartTag($start_tag="[gap]")
Sets the start tag of a cloze gap.
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...
cleanQuestiontext($text)
Cleans cloze question text to remove attributes or tags from older ILIAS versions.
getIdenticalScoring()
Returns the identical scoring status of the question.
getClozeTextForHTMLOutput()
Returns the cloze text as HTML (with optional nl2br) Fix for Mantis 29987: We assume Tiny embeds any ...
Abstract basic class which is to be extended by the concrete assessment question type classes.
float $points
The maximum available points for the question.
setOriginalId(?int $original_id)
string $question
The question text.
ilDBInterface $db
static logAction(string $logtext, int $active_id, int $question_id)
setId(int $id=-1)
prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
ILIAS Refinery Factory $refinery
setQuestion(string $question="")
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
static _getOriginalId(int $question_id)
saveQuestionDataToDb(int $original_id=-1)
setAuthor(string $author="")
bool $shuffle
Indicates whether the answers will be shuffled or not.
setComment(string $comment="")
setObjId(int $obj_id=0)
getSolutionMaxPass(int $active_id)
setOwner(int $owner=-1)
setNrOfTries(int $a_nr_of_tries)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
isAdditionalContentEditingModePageObject()
setTitle(string $title="")
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
lookupMaxStep(int $active_id, int $pass)
setPoints(float $points)
ensureNonNegativePoints($points)
const FB_MODE_GAP_QUESTION
constants for different feedback modes (per gap or per gap-answers/options)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setCell($a_row, $a_col, $a_value, $datatype=null)
setBold(string $a_coords)
Set cell(s) to bold.
getColumnCoord(int $a_col)
Get column "name" from number.
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
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...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static strToLower(string $a_string)
Definition: class.ilStr.php:72
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static stripSlashes(string $a_str, bool $a_strip_html=true, string $a_allow="")
global $DIC
Definition: feed.php:28
$mobs
Definition: imgupload.php:70
const TEXTGAP_RATING_LEVENSHTEIN5
const TEXTGAP_RATING_LEVENSHTEIN4
const TEXTGAP_RATING_LEVENSHTEIN3
const TEXTGAP_RATING_CASESENSITIVE
const CLOZE_NUMERIC
const TEXTGAP_RATING_LEVENSHTEIN2
const TEXTGAP_RATING_CASEINSENSITIVE
const CLOZE_SELECT
const TEXTGAP_RATING_LEVENSHTEIN1
const CLOZE_TEXT
Cloze question constants.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getUserQuestionResult($active_id, $pass)
Get the user solution for a question by active_id and the test pass.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$index
Definition: metadata.php:145
$i
Definition: metadata.php:41
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
string $key
Consumer key/client ID value.
Definition: System.php:193
$query