ILIAS  trunk Revision v12.0_alpha-1227-g7ff6d300864
class.assClozeTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
25use ILIAS\Refinery\Random\Group as RandomGroup;
26
39{
44 public array $gaps = [];
45
53 protected $gap_combinations = [];
54 protected bool $gap_combinations_exist = false;
55 private string $start_tag = '[gap]';
56 private string $end_tag = '[/gap]';
57
67
75 protected bool $identical_scoring = true;
76 protected ?int $fixed_text_length = null;
77 protected string $cloze_text = '';
80 private RandomGroup $randomGroup;
81
82 public function __construct(
83 string $title = "",
84 string $comment = "",
85 string $author = "",
86 int $owner = -1,
87 string $question = ""
88 ) {
89 global $DIC;
91 $this->setQuestion($question); // @TODO: Should this be $question?? See setter for why this is not trivial.
92 $this->randomGroup = $DIC->refinery()->random();
93 }
94
100 public function isComplete(): bool
101 {
102 if ($this->getTitle() !== ''
103 && $this->getAuthor()
104 && $this->getClozeText()
105 && count($this->getGaps())
106 && $this->getMaximumPoints() > 0) {
107 return true;
108 }
109 return false;
110 }
111
119 public function cleanQuestiontext($text): string
120 {
121 if ($text === null) {
122 return '';
123 }
124 // fau: fixGapReplace - mask dollars for replacement
125 $text = str_replace('$', 'GAPMASKEDDOLLAR', $text);
126 $text = preg_replace("/\[gap[^\]]*?\]/", "[gap]", $text);
127 $text = preg_replace("/<gap([^>]*?)>/", "[gap]", $text);
128 $text = str_replace("</gap>", "[/gap]", $text);
129 $text = str_replace('GAPMASKEDDOLLAR', '$', $text);
130 // fau.
131 return $text;
132 }
133
134 public function replaceFirstGap(
135 string $gaptext,
136 string $content
137 ): string {
138 $output = preg_replace(
139 '/\[gap\].*?\[\/gap\]/',
140 str_replace('$', 'GAPMASKEDDOLLAR', $content),
141 $gaptext,
142 1
143 );
144 return str_replace('GAPMASKEDDOLLAR', '$', $output);
145 }
146
147 public function loadFromDb(int $question_id): void
148 {
149 $result = $this->db->queryF(
150 "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",
151 ["integer"],
152 [$question_id]
153 );
154 if ($result->numRows() == 1) {
155 $data = $this->db->fetchAssoc($result);
156 $this->setId($question_id);
157 $this->setNrOfTries($data['nr_of_tries']);
158 $this->setObjId($data["obj_fi"]);
159 $this->setTitle((string) $data["title"]);
160 $this->setComment((string) $data["description"]);
161 $this->setOriginalId($data["original_id"]);
162 $this->setAuthor($data["author"]);
163 $this->setPoints($data["points"]);
164 $this->setOwner($data["owner"]);
165 $this->setQuestion($this->cleanQuestiontext($data["question_text"]));
166 $this->setClozeText($data['cloze_text'] ?? '');
167 $this->setFixedTextLength($data["fixed_textlen"]);
168 $this->setIdenticalScoring(($data['tstamp'] === 0) ? true : (bool) $data['identical_scoring']);
169 $this->setFeedbackMode($data['feedback_mode'] === null ? ilAssClozeTestFeedback::FB_MODE_GAP_QUESTION : $data['feedback_mode']);
170
171 try {
172 $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
174 $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance());
175 }
176
177 $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1);
178 $this->cloze_text = ilRTE::_replaceMediaObjectImageSrc($this->cloze_text, 1);
179 $this->setTextgapRating($data["textgap_rating"]);
180
181 try {
182 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
184 }
185
186 $result = $this->db->queryF(
187 "SELECT * FROM qpl_a_cloze WHERE question_fi = %s ORDER BY gap_id, aorder ASC",
188 ["integer"],
189 [$question_id]
190 );
191 if ($result->numRows() > 0) {
192 $this->gaps = [];
193 while ($data = $this->db->fetchAssoc($result)) {
194 switch ($data["cloze_type"]) {
196 if (!array_key_exists($data["gap_id"], $this->gaps)) {
197 $this->gaps[$data["gap_id"]] = new assClozeGap(assClozeGap::TYPE_TEXT);
198 }
199 $answer = new assAnswerCloze(
200 $data["answertext"],
201 $data["points"],
202 $data["aorder"]
203 );
204 $this->gaps[$data["gap_id"]]->setGapSize((int) $data['gap_size']);
205
206 $this->gaps[$data["gap_id"]]->addItem($answer);
207 break;
209 if (!array_key_exists($data["gap_id"], $this->gaps)) {
210 $this->gaps[$data["gap_id"]] = new assClozeGap(assClozeGap::TYPE_SELECT);
211 $this->gaps[$data["gap_id"]]->setShuffle($data["shuffle"]);
212 }
213 $answer = new assAnswerCloze(
214 $data["answertext"],
215 $data["points"],
216 $data["aorder"]
217 );
218 $this->gaps[$data["gap_id"]]->addItem($answer);
219 break;
221 if (!array_key_exists($data["gap_id"], $this->gaps)) {
222 $this->gaps[$data["gap_id"]] = new assClozeGap(assClozeGap::TYPE_NUMERIC);
223 }
224 $answer = new assAnswerCloze(
225 $data["answertext"],
226 $data["points"],
227 $data["aorder"]
228 );
229 $this->gaps[$data["gap_id"]]->setGapSize((int) $data['gap_size']);
230 $answer->setLowerBound($data["lowerlimit"]);
231 $answer->setUpperBound($data["upperlimit"]);
232 $this->gaps[$data["gap_id"]]->addItem($answer);
233 break;
234 }
235 }
236 }
237 }
238 $check_for_gap_combinations = (new assClozeGapCombination($this->db))->loadFromDb($question_id);
239 if (count($check_for_gap_combinations) != 0) {
240 $this->setGapCombinationsExists(true);
241 $this->setGapCombinations($check_for_gap_combinations);
242 }
243 parent::loadFromDb($question_id);
244 }
245
246 public function saveToDb(?int $original_id = null): void
247 {
248 $this->saveQuestionDataToDb($original_id);
249 $this->saveAdditionalQuestionDataToDb();
250 $this->saveAnswerSpecificDataToDb();
251
252 parent::saveToDb();
253 }
254
255 public function saveAnswerSpecificDataToDb(): void
256 {
257 $this->db->manipulateF(
258 "DELETE FROM qpl_a_cloze WHERE question_fi = %s",
259 [ "integer" ],
260 [ $this->getId() ]
261 );
262
263 foreach ($this->gaps as $key => $gap) {
264 $this->saveClozeGapItemsToDb($gap, $key);
265 }
266 }
267
268 public function saveAdditionalQuestionDataToDb(): void
269 {
270 $this->db->manipulateF(
271 "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
272 [ "integer" ],
273 [ $this->getId() ]
274 );
275
276 $this->db->insert($this->getAdditionalTableName(), [
277 'question_fi' => ['integer', $this->getId()],
278 'textgap_rating' => ['text', $this->getTextgapRating()],
279 'identical_scoring' => ['text', $this->getIdenticalScoring()],
280 'fixed_textlen' => ['integer', $this->getFixedTextLength() ? $this->getFixedTextLength() : null],
281 'cloze_text' => ['text', ilRTE::_replaceMediaObjectImageSrc($this->getClozeText(), 0)],
282 'feedback_mode' => ['text', $this->getFeedbackMode()]
283 ]);
284 }
285 protected function saveClozeGapItemsToDb(
286 assClozeGap $gap,
287 int $key
288 ): void {
289 foreach ($gap->getItems($this->getShuffler()) as $item) {
290 $next_id = $this->db->nextId('qpl_a_cloze');
291 switch ($gap->getType()) {
293 $this->saveClozeTextGapRecordToDb($next_id, $key, $item, $gap);
294 break;
296 $this->saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap);
297 break;
299 $this->saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap);
300 break;
301 }
302 }
303 }
304
305 protected function saveClozeTextGapRecordToDb(
306 int $next_id,
307 int $key,
308 assAnswerCloze $item,
309 assClozeGap $gap
310 ): void {
311 $this->db->manipulateF(
312 '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)',
313 [
314 ilDBConstants::T_INTEGER,
315 ilDBConstants::T_INTEGER,
316 ilDBConstants::T_INTEGER,
317 ilDBConstants::T_TEXT,
318 ilDBConstants::T_FLOAT,
319 ilDBConstants::T_INTEGER,
320 ilDBConstants::T_TEXT,
321 ilDBConstants::T_INTEGER
322 ],
323 [
324 $next_id,
325 $this->getId(),
326 $key,
327 $item->getAnswertext(),
328 $item->getPoints(),
329 $item->getOrder(),
330 $gap->getType(),
331 $gap->getGapSize()
332 ]
333 );
334 }
335
337 int $next_id,
338 int $key,
339 assAnswerCloze $item,
340 assClozeGap $gap
341 ): void {
342 $this->db->manipulateF(
343 '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)',
344 [
345 ilDBConstants::T_INTEGER,
346 ilDBConstants::T_INTEGER,
347 ilDBConstants::T_INTEGER,
348 ilDBConstants::T_TEXT,
349 ilDBConstants::T_FLOAT,
350 ilDBConstants::T_INTEGER,
351 ilDBConstants::T_TEXT,
352 ilDBConstants::T_TEXT
353 ],
354 [
355 $next_id,
356 $this->getId(),
357 $key,
358 $item->getAnswertext(),
359 $item->getPoints(),
360 $item->getOrder(),
361 $gap->getType(),
362 $gap->getShuffle() ? '1' : '0'
363 ]
364 );
365 }
366
368 int $next_id,
369 int $key,
370 assAnswerCloze $item,
371 assClozeGap $gap
372 ): void {
373 $eval = new EvalMath();
374 $eval->suppress_errors = true;
375 $this->db->manipulateF(
376 '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)',
377 [
388 ],
389 [
390 $next_id,
391 $this->getId(),
392 $key,
393 $item->getAnswertext(),
394 $item->getPoints(),
395 $item->getOrder(),
396 $gap->getType(),
397 ($eval->e($item->getLowerBound()) !== false && ($item->getLowerBound() ?? '') !== '')
398 ? $item->getLowerBound()
399 : $item->getAnswertext(),
400 ($eval->e($item->getUpperBound()) !== false && ($item->getUpperBound() ?? '') !== '')
401 ? $item->getUpperBound()
402 : $item->getAnswertext(),
403 $gap->getGapSize()
404 ]
405 );
406 }
407
408 public function getGaps(): array
409 {
410 return $this->gaps;
411 }
412
413 public function flushGaps(): void
414 {
415 $this->gaps = [];
416 }
417
418 public function setClozeText(string $cloze_text = ''): void
419 {
420 $this->gaps = [];
421 $this->cloze_text = $this->cleanQuestiontext($cloze_text);
422 $this->createGapsFromQuestiontext();
423 }
424
425 public function setClozeTextValue($cloze_text = ""): void
426 {
427 $this->cloze_text = $cloze_text;
428 }
429
437 public function getClozeText(): string
438 {
439 return $this->cloze_text;
440 }
441
450 public function getClozeTextForHTMLOutput(): string
451 {
452 $gaps = [];
453 preg_match_all('/\[gap\].*?\[\/gap\]/', $this->getClozeText(), $gaps);
454 $string_with_replaced_gaps = str_replace($gaps[0], '######GAP######', $this->getClozeText());
455 $cleaned_text = $this->getHtmlQuestionContentPurifier()->purify(
456 $string_with_replaced_gaps
457 );
458 $cleaned_text_with_gaps = preg_replace_callback('/######GAP######/', function ($match) use (&$gaps) {
459 return array_shift($gaps[0]);
460 }, $cleaned_text);
461
462 if ($this->isAdditionalContentEditingModePageObject()
463 || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
464 $cleaned_text_with_gaps = nl2br($cleaned_text_with_gaps);
465 }
466
467 return ilLegacyFormElementsUtil::prepareTextareaOutput($cleaned_text_with_gaps, true);
468 }
469
477 public function getStartTag(): string
478 {
479 return $this->start_tag;
480 }
481
489 public function setStartTag($start_tag = "[gap]"): void
490 {
491 $this->start_tag = $start_tag;
492 }
493
501 public function getEndTag(): string
502 {
503 return $this->end_tag;
504 }
505
513 public function setEndTag($end_tag = "[/gap]"): void
514 {
515 $this->end_tag = $end_tag;
516 }
517
521 public function getFeedbackMode(): string
522 {
523 return $this->feedbackMode;
524 }
525
529 public function setFeedbackMode($feedbackMode): void
530 {
531 $this->feedbackMode = $feedbackMode;
532 }
533
540 public function createGapsFromQuestiontext(): void
541 {
542 $search_pattern = "|\[gap\](.*?)\[/gap\]|i";
543 preg_match_all($search_pattern, $this->getClozeText(), $found);
544 $this->gaps = [];
545 if (count($found[0])) {
546 foreach ($found[1] as $gap_index => $answers) {
547 // create text gaps by default
549 $textparams = preg_split("/(?<!\\\\),/", $answers);
550 foreach ($textparams as $key => $value) {
551 $answer = new assAnswerCloze($value, 0, $key);
552 $gap->addItem($answer);
553 }
554 $this->gaps[$gap_index] = $gap;
555 }
556 }
557 }
558
564 public function setGapType($gap_index, $gap_type): void
565 {
566 if (array_key_exists($gap_index, $this->gaps)) {
567 $this->gaps[$gap_index]->setType($gap_type);
568 }
569 }
570
580 public function setGapShuffle($gap_index = 0, $shuffle = 1): void
581 {
582 if (array_key_exists($gap_index, $this->gaps)) {
583 $this->gaps[$gap_index]->setShuffle($shuffle);
584 }
585 }
586
593 public function clearGapAnswers(): void
594 {
595 foreach ($this->gaps as $gap_index => $gap) {
596 $this->gaps[$gap_index]->clearItems();
597 }
598 }
599
607 public function getGapCount(): int
608 {
609 if (is_array($this->gaps)) {
610 return count($this->gaps);
611 } else {
612 return 0;
613 }
614 }
615
626 public function addGapAnswer($gap_index, $order, $answer): void
627 {
628 if (array_key_exists($gap_index, $this->gaps)) {
629 if ($this->gaps[$gap_index]->getType() == assClozeGap::TYPE_NUMERIC) {
630 // only allow notation with "." for real numbers
631 $answer = str_replace(",", ".", $answer);
632 }
633 $this->gaps[$gap_index]->addItem(new assAnswerCloze(trim($answer), 0, $order));
634 }
635 }
636
637 public function getGap(int $gap_index = 0): ?assClozeGap
638 {
639 if (array_key_exists($gap_index, $this->gaps)) {
640 return $this->gaps[$gap_index];
641 }
642 return null;
643 }
644
645 public function setGapSize($gap_index, $size): void
646 {
647 if (array_key_exists($gap_index, $this->gaps)) {
648 $this->gaps[$gap_index]->setGapSize((int) $size);
649 }
650 }
651
662 public function setGapAnswerPoints($gap_index, $order, $points): void
663 {
664 if (array_key_exists($gap_index, $this->gaps)) {
665 $this->gaps[$gap_index]->setItemPoints($order, $points);
666 }
667 }
668
677 public function addGapText($gap_index): void
678 {
679 if (array_key_exists($gap_index, $this->gaps)) {
680 $answer = new assAnswerCloze(
681 "",
682 0,
683 $this->gaps[$gap_index]->getItemCount()
684 );
685 $this->gaps[$gap_index]->addItem($answer);
686 }
687 }
688
697 public function addGapAtIndex($gap, $index): void
698 {
699 $this->gaps[$index] = $gap;
700 }
701
712 public function setGapAnswerLowerBound($gap_index, $order, $bound): void
713 {
714 if (array_key_exists($gap_index, $this->gaps)) {
715 $this->gaps[$gap_index]->setItemLowerBound($order, $bound);
716 }
717 }
718
729 public function setGapAnswerUpperBound($gap_index, $order, $bound): void
730 {
731 if (array_key_exists($gap_index, $this->gaps)) {
732 $this->gaps[$gap_index]->setItemUpperBound($order, $bound);
733 }
734 }
735
742 public function getMaximumPoints(): float
743 {
744 $assClozeGapCombinationObj = new assClozeGapCombination($this->db);
745 $points = 0;
746 $gaps_used_in_combination = [];
747 if ($this->gap_combinations_exist) {
748 $points = $assClozeGapCombinationObj->getMaxPointsForCombination($this->getId());
749 $gaps_used_in_combination = $assClozeGapCombinationObj->getGapsWhichAreUsedInCombination($this->getId());
750 }
751 foreach ($this->gaps as $gap_index => $gap) {
752 if (!array_key_exists($gap_index, $gaps_used_in_combination)) {
753 if ($gap->getType() == assClozeGap::TYPE_TEXT) {
754 $gap_max_points = 0;
755 foreach ($gap->getItems($this->getShuffler()) as $item) {
756 if ($item->getPoints() > $gap_max_points) {
757 $gap_max_points = $item->getPoints();
758 }
759 }
760 $points += $gap_max_points;
761 } elseif ($gap->getType() == assClozeGap::TYPE_SELECT) {
762 $srpoints = 0;
763 foreach ($gap->getItems($this->getShuffler()) as $item) {
764 if ($item->getPoints() > $srpoints) {
765 $srpoints = $item->getPoints();
766 }
767 }
768 $points += $srpoints;
769 } elseif ($gap->getType() == assClozeGap::TYPE_NUMERIC) {
770 $numpoints = 0;
771 foreach ($gap->getItems($this->getShuffler()) as $item) {
772 if ($item->getPoints() > $numpoints) {
773 $numpoints = $item->getPoints();
774 }
775 }
776 $points += $numpoints;
777 }
778 }
779 }
780
781 return $points;
782 }
783
785 \assQuestion $target
786 ): \assQuestion {
787 if ($this->gap_combinations_exist) {
788 $gap_combination = new assClozeGapCombination($this->db);
789 $gap_combination->clearGapCombinationsFromDb($target->getId());
790 $gap_combination->importGapCombinationToDb(
791 $target->getId(),
792 $this->gap_combinations,
793 );
794
795 // Mantis 46713: The maximum points may have changed due to the combinations,
796 // so the question must be saved again
797 $target->saveToDb();
798 }
799
800 return $target;
801 }
802
808 public function updateClozeTextFromGaps(): void
809 {
810 $output = $this->getClozeText();
811 foreach ($this->getGaps() as $gap_index => $gap) {
812 $answers = [];
813 foreach ($gap->getItemsRaw() as $item) {
814 array_push($answers, str_replace([',', '['], ["\\,", '[&hairsp;'], $item->getAnswerText()));
815 }
816 // fau: fixGapReplace - use replace function
817 $output = $this->replaceFirstGap($output, "[_gap]" . ilLegacyFormElementsUtil::prepareTextareaOutput(join(",", $answers), true) . "[/_gap]");
818 // fau.
819 }
820 $output = str_replace("_gap]", "gap]", $output);
821 $this->cloze_text = $output;
822 }
823
833 public function deleteAnswerText($gap_index, $answer_index): void
834 {
835 if (array_key_exists($gap_index, $this->gaps)) {
836 if ($this->gaps[$gap_index]->getItemCount() == 1) {
837 // this is the last answer text => remove the gap
838 $this->deleteGap($gap_index);
839 } else {
840 // remove the answer text
841 $this->gaps[$gap_index]->deleteItem($answer_index);
842 $this->updateClozeTextFromGaps();
843 }
844 }
845 }
846
855 public function deleteGap($gap_index): void
856 {
857 if (array_key_exists($gap_index, $this->gaps)) {
858 $output = $this->getClozeText();
859 foreach ($this->getGaps() as $replace_gap_index => $gap) {
860 $answers = [];
861 foreach ($gap->getItemsRaw() as $item) {
862 array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
863 }
864 if ($replace_gap_index == $gap_index) {
865 // fau: fixGapReplace - use replace function
866 $output = $this->replaceFirstGap($output, '');
867 // fau.
868 } else {
869 // fau: fixGapReplace - use replace function
870 $output = $this->replaceFirstGap($output, "[_gap]" . join(",", $answers) . "[/_gap]");
871 // fau.
872 }
873 }
874 $output = str_replace("_gap]", "gap]", $output);
875 $this->cloze_text = $output;
876 unset($this->gaps[$gap_index]);
877 $this->gaps = array_values($this->gaps);
878 }
879 }
880
890 public function getTextgapPoints($a_original, $a_entered, $max_points): float
891 {
892 global $DIC;
893 $refinery = $DIC->refinery();
894 $result = 0;
895 $gaprating = $this->getTextgapRating();
896
897 switch ($gaprating) {
899 if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) {
900 $result = $max_points;
901 }
902 break;
904 if (strcmp($a_original, $a_entered) == 0) {
905 $result = $max_points;
906 }
907 break;
909 $transformation = $refinery->string()->levenshtein()->standard($a_original, 1);
910 break;
912 $transformation = $refinery->string()->levenshtein()->standard($a_original, 2);
913 break;
915 $transformation = $refinery->string()->levenshtein()->standard($a_original, 3);
916 break;
918 $transformation = $refinery->string()->levenshtein()->standard($a_original, 4);
919 break;
921 $transformation = $refinery->string()->levenshtein()->standard($a_original, 5);
922 break;
923 }
924
925 // run answers against Levenshtein2 methods
926 if (isset($transformation) && $transformation->transform($a_entered) >= 0) {
927 $result = $max_points;
928 }
929 return $result;
930 }
931
932
942 public function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound): float
943 {
944 $eval = new EvalMath();
945 $eval->suppress_errors = true;
946 $result = 0.0;
947
948 if ($eval->e($a_entered) === false) {
949 return 0.0;
950 } elseif (($eval->e($lowerBound) !== false) && ($eval->e($upperBound) !== false)) {
951 if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
952 $result = $max_points;
953 }
954 } elseif ($eval->e($lowerBound) !== false) {
955 if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) {
956 $result = $max_points;
957 }
958 } elseif ($eval->e($upperBound) !== false) {
959 if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
960 $result = $max_points;
961 }
962 } elseif ($eval->e($a_entered) == $eval->e($a_original)) {
963 $result = $max_points;
964 }
965 return $result;
966 }
967
968 public function checkForValidFormula(string $value): int
969 {
970 return preg_match("/^-?(\\d*)(,|\\.|\\/){0,1}(\\d*)$/", $value, $matches);
971 }
972
973 public function calculateReachedPoints(
974 int $active_id,
975 ?int $pass = null,
976 bool $authorized_solution = true
977 ): float {
978 $user_result = $this->fetchUserResult($active_id, $pass, $authorized_solution);
979 return $this->calculateReachedPointsForSolution($user_result);
980 }
981
982 public function getUserResultDetails(
983 int $active_id,
984 ?int $pass = null,
985 bool $authorized_solution = true
986 ): array {
987 $user_result = $this->fetchUserResult($active_id, $pass, $authorized_solution);
988 $detailed = [];
989 $this->calculateReachedPointsForSolution($user_result, $detailed);
990 return $detailed;
991 }
992
993 private function fetchUserResult(
994 int $active_id,
995 ?int $pass
996 ): array {
997 if (is_null($pass)) {
998 $pass = $this->getSolutionMaxPass($active_id);
999 }
1000
1001 $result = $this->getCurrentSolutionResultSet($active_id, $pass, true);
1002 $user_result = [];
1003 while ($data = $this->db->fetchAssoc($result)) {
1004 if ($data['value2'] === '') {
1005 continue;
1006 }
1007 $user_result[$data['value1']] = [
1008 'gap_id' => $data['value1'],
1009 'value' => $data['value2']
1010 ];
1011 }
1012
1013 ksort($user_result);
1014 return $user_result;
1015 }
1016
1017 protected function isValidNumericSubmitValue($submittedValue): bool
1018 {
1019 if (is_numeric($submittedValue)) {
1020 return true;
1021 }
1022
1023 if (preg_match('/^[-+]{0,1}\d+\/\d+$/', $submittedValue)) {
1024 return true;
1025 }
1026
1027 return false;
1028 }
1029
1030 public function fetchSolutionSubmit(): array
1031 {
1032 $solution_submit = [];
1033 $post_wrapper = $this->dic->http()->wrapper()->post();
1034 foreach ($this->getGaps() as $index => $gap) {
1035 if (!$post_wrapper->has("gap_$index")) {
1036 continue;
1037 }
1038 $value = trim($post_wrapper->retrieve(
1039 "gap_$index",
1040 $this->dic->refinery()->kindlyTo()->string()
1041 ));
1042 if ($value === '') {
1043 continue;
1044 }
1045
1046 if ($gap->getType() === assClozeGap::TYPE_SELECT && $value === '-1') {
1047 continue;
1048 }
1049
1050 if ($gap->getType() === assClozeGap::TYPE_NUMERIC) {
1051 $value = str_replace(',', '.', $value);
1052 if (!is_numeric($value)) {
1053 $value = null;
1054 }
1055 }
1056
1057 $solution_submit[$index] = $value;
1058 }
1059
1060 return $solution_submit;
1061 }
1062
1063 protected function getSolutionSubmit(): array
1064 {
1065 return $this->fetchSolutionSubmit();
1066 }
1067
1068 public function saveWorkingData(
1069 int $active_id,
1070 ?int $pass = null,
1071 bool $authorized = true
1072 ): bool {
1073 if (is_null($pass)) {
1074 $pass = ilObjTest::_getPass($active_id);
1075 }
1076
1077 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
1078 function () use ($active_id, $pass, $authorized) {
1079 $this->removeCurrentSolution($active_id, $pass, $authorized);
1080
1081 foreach ($this->fetchSolutionSubmit() as $key => $value) {
1082 if ($value === null || $value === '') {
1083 continue;
1084 }
1085 $gap = $this->getGap($key);
1086 if ($gap === null
1087 || $gap->getType() === assClozeGap::TYPE_SELECT && $value === -1) {
1088 continue;
1089 }
1090 $this->saveCurrentSolution($active_id, $pass, $key, $value, $authorized);
1091 }
1092 }
1093 );
1094
1095 return true;
1096 }
1097
1104 public function getQuestionType(): string
1105 {
1106 return "assClozeTest";
1107 }
1108
1116 public function getTextgapRating(): string
1117 {
1118 return $this->textgap_rating;
1119 }
1120
1128 public function setTextgapRating($a_textgap_rating): void
1129 {
1130 switch ($a_textgap_rating) {
1138 $this->textgap_rating = $a_textgap_rating;
1139 break;
1140 default:
1141 $this->textgap_rating = assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE;
1142 break;
1143 }
1144 }
1145
1153 public function getIdenticalScoring(): bool
1154 {
1155 return $this->identical_scoring;
1156 }
1157
1165 public function setIdenticalScoring(bool $identical_scoring): void
1166 {
1167 $this->identical_scoring = $identical_scoring;
1168 }
1169
1176 public function getAdditionalTableName(): string
1177 {
1178 return "qpl_qst_cloze";
1179 }
1180
1181 public function getAnswerTableName(): array
1182 {
1183 return ["qpl_a_cloze",'qpl_a_cloze_combi_res'];
1184 }
1185
1192 public function setFixedTextLength(?int $fixed_text_length): void
1193 {
1194 $this->fixed_text_length = $fixed_text_length;
1195 }
1196
1203 public function getFixedTextLength(): ?int
1204 {
1205 return $this->fixed_text_length;
1206 }
1207
1216 public function getMaximumGapPoints($gap_index)
1217 {
1218 $points = 0;
1219 $gap_max_points = 0;
1220 if (array_key_exists($gap_index, $this->gaps)) {
1221 $gap = &$this->gaps[$gap_index];
1222 foreach ($gap->getItems($this->getShuffler()) as $answer) {
1223 if ($answer->getPoints() > $gap_max_points) {
1224 $gap_max_points = $answer->getPoints();
1225 }
1226 }
1227 $points += $gap_max_points;
1228 }
1229 return $points;
1230 }
1231
1236 public function getRTETextWithMediaObjects(): string
1237 {
1238 return parent::getRTETextWithMediaObjects() . $this->getClozeText();
1239 }
1240 public function getGapCombinationsExists(): bool
1241 {
1242 return $this->gap_combinations_exist;
1243 }
1244
1245 public function getGapCombinations(): array
1246 {
1247 return $this->gap_combinations;
1248 }
1249
1250 public function setGapCombinationsExists($value): void
1251 {
1252 $this->gap_combinations_exist = $value;
1253 }
1254
1255 public function setGapCombinations($value): void
1256 {
1257 $this->gap_combinations = $value;
1258 }
1259
1264 {
1265 // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1266 //$this->setClozeText( $migrator->migrateToLmContent($this->getClozeText()) );
1267 $this->cloze_text = $migrator->migrateToLmContent($this->getClozeText());
1268 // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1269 }
1270
1274 public function toJSON(): string
1275 {
1276 $result = [
1277 'id' => $this->getId(),
1278 'type' => (string) $this->getQuestionType(),
1279 'title' => $this->getTitleForHTMLOutput(),
1280 'question' => $this->formatSAQuestion($this->getQuestion()),
1281 'clozetext' => $this->formatSAQuestion($this->getClozeText()),
1282 'nr_of_tries' => $this->getNrOfTries(),
1283 'shuffle' => $this->getShuffle(),
1284 'feedback' => [
1285 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1286 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1287 ]
1288 ];
1289
1290 $gaps = [];
1291 foreach ($this->getGaps() as $key => $gap) {
1292 $items = [];
1293 foreach ($gap->getItems($this->getShuffler()) as $item) {
1294 $jitem = [];
1295 $jitem['points'] = $item->getPoints();
1296 $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1297 $jitem['order'] = $item->getOrder();
1298 if ($gap->getType() == assClozeGap::TYPE_NUMERIC) {
1299 $jitem['lowerbound'] = $item->getLowerBound();
1300 $jitem['upperbound'] = $item->getUpperBound();
1301 } else {
1302 $jitem['value'] = trim($jitem['value']);
1303 }
1304 array_push($items, $jitem);
1305 }
1306
1307 if ($gap->getGapSize() && ($gap->getType() == assClozeGap::TYPE_TEXT || $gap->getType() == assClozeGap::TYPE_NUMERIC)) {
1308 $jgap['size'] = $gap->getGapSize();
1309 }
1310
1311 $jgap['shuffle'] = $gap->getShuffle();
1312 $jgap['type'] = $gap->getType();
1313 $jgap['item'] = $items;
1314
1315 array_push($gaps, $jgap);
1316 }
1317 $result['gaps'] = $gaps;
1318 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1319 $result['mobs'] = $mobs;
1320 return json_encode($result);
1321 }
1322
1323 public function getOperators(string $expression): array
1324 {
1326 }
1327
1328 public function getExpressionTypes(): array
1329 {
1330 return [
1336 ];
1337 }
1338
1339 public function getUserQuestionResult(
1340 int $active_id,
1341 int $pass
1343 $result = new ilUserQuestionResult($this, $active_id, $pass);
1344
1345 $maxStep = $this->lookupMaxStep($active_id, $pass);
1346 if ($maxStep > 0) {
1347 $data = $this->db->queryF(
1348 "
1349 SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1350 FROM tst_solutions sol
1351 INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1352 WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s AND sol.step = %s
1353 GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1354 ",
1355 ["integer", "integer", "integer","integer"],
1356 [$active_id, $pass, $this->getId(), $maxStep]
1357 );
1358 } else {
1359 $data = $this->db->queryF(
1360 "
1361 SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1362 FROM tst_solutions sol
1363 INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1364 WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s
1365 GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1366 ",
1367 ["integer", "integer", "integer"],
1368 [$active_id, $pass, $this->getId()]
1369 );
1370 }
1371
1372 while ($row = $this->db->fetchAssoc($data)) {
1373 if ($row["cloze_type"] == 1) {
1374 $row["value2"]++;
1375 }
1376 $result->addKeyValue($row["val"], $row["value2"]);
1377 }
1378
1379 $points = $this->calculateReachedPoints($active_id, $pass);
1380 $max_points = $this->getMaximumPoints();
1381
1382 $result->setReachedPercentage(($points / $max_points) * 100);
1383
1384 return $result;
1385 }
1386
1395 public function getAvailableAnswerOptions($index = null)
1396 {
1397 if ($index !== null) {
1398 return $this->getGap($index);
1399 } else {
1400 return $this->getGaps();
1401 }
1402 }
1403
1404 public function calculateCombinationResult($user_result): array
1405 {
1406 $points = 0;
1407
1408 $assClozeGapCombinationObj = new assClozeGapCombination($this->db);
1409 $gap_used_in_combination = [];
1410 if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1411 $combinations_for_question = $assClozeGapCombinationObj->getCleanCombinationArray($this->getId());
1412 $gap_answers = [];
1413
1414 foreach ($user_result as $user_result_build_list) {
1415 if (is_array($user_result_build_list)) {
1416 $gap_answers[$user_result_build_list['gap_id']] = $user_result_build_list['value'];
1417 }
1418 }
1419
1420 foreach ($combinations_for_question as $combination) {
1421 foreach ($combination as $row_key => $row_answers) {
1422 $combination_fulfilled = true;
1423 $points_for_combination = $row_answers['points'];
1424 foreach ($row_answers as $gap_key => $combination_gap_answer) {
1425 if ($gap_key !== 'points') {
1426 $gap_used_in_combination[$gap_key] = $gap_key;
1427 }
1428 if ($combination_fulfilled && array_key_exists($gap_key, $gap_answers)) {
1429 switch ($combination_gap_answer['type']) {
1431 $is_text_gap_correct = $this->getTextgapPoints($gap_answers[$gap_key], $combination_gap_answer['answer'], 1);
1432 if ($is_text_gap_correct != 1) {
1433 $combination_fulfilled = false;
1434 }
1435 break;
1437 $answer = $this->gaps[$gap_key]->getItem($gap_answers[$gap_key]);
1438 $answertext = $answer?->getAnswertext();
1439 if ($answertext != $combination_gap_answer['answer']) {
1440 $combination_fulfilled = false;
1441 }
1442 break;
1444 $answer = $this->gaps[$gap_key]->getItem(0);
1445 if ($combination_gap_answer['answer'] != 'out_of_bound') {
1446 $is_numeric_gap_correct = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1447 if ($is_numeric_gap_correct != 1) {
1448 $combination_fulfilled = false;
1449 }
1450 } else {
1451 $wrong_is_the_new_right = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1452 if ($wrong_is_the_new_right == 1) {
1453 $combination_fulfilled = false;
1454 }
1455 }
1456 break;
1457 }
1458 } else {
1459 if ($gap_key !== 'points') {
1460 $combination_fulfilled = false;
1461 }
1462 }
1463 }
1464 if ($combination_fulfilled) {
1465 $points += $points_for_combination;
1466 }
1467 }
1468 }
1469 }
1470 return [$points, $gap_used_in_combination];
1471 }
1476 protected function calculateReachedPointsForSolution(?array $user_result, array &$detailed = []): float
1477 {
1478 $points = 0.0;
1479
1480 $assClozeGapCombinationObj = new assClozeGapCombination($this->db);
1481 $combinations[1] = [];
1482 if ($this->gap_combinations_exist) {
1483 $combinations = $this->calculateCombinationResult($user_result);
1484 $points = $combinations[0];
1485 }
1486
1487 $solution_values_text = []; // for identical scoring checks
1488 $solution_values_select = []; // for identical scoring checks
1489 $solution_values_numeric = []; // for identical scoring checks
1490 foreach ($user_result as $gap_id => $value) {
1491 if (is_string($value)) {
1492 $value = ["value" => $value];
1493 }
1494
1495 if (array_key_exists($gap_id, $this->gaps) && !array_key_exists($gap_id, $combinations[1])) {
1496 switch ($this->gaps[$gap_id]->getType()) {
1498 $gappoints = 0.0;
1499 for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1500 $answer = $this->gaps[$gap_id]->getItem($order);
1501 $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1502 if ($gotpoints > $gappoints) {
1503 $gappoints = $gotpoints;
1504 }
1505 }
1506 if (!$this->getIdenticalScoring()) {
1507 // check if the same solution text was already entered
1508 if ((in_array($value["value"], $solution_values_text)) && ($gappoints > 0.0)) {
1509 $gappoints = 0.0;
1510 }
1511 }
1512 $points += $gappoints;
1513 $detailed[$gap_id] = ["points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0.0) ? true : false];
1514 array_push($solution_values_text, $value["value"]);
1515 break;
1517 $gappoints = 0.0;
1518 for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1519 $answer = $this->gaps[$gap_id]->getItem($order);
1520 $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1521 if ($gotpoints > $gappoints) {
1522 $gappoints = $gotpoints;
1523 }
1524 }
1525 if (!$this->getIdenticalScoring()) {
1526 // check if the same solution value was already entered
1527 $eval = new EvalMath();
1528 $eval->suppress_errors = true;
1529 $found_value = false;
1530 foreach ($solution_values_numeric as $solval) {
1531 if ($eval->e($solval) == $eval->e($value["value"])) {
1532 $found_value = true;
1533 }
1534 }
1535 if ($found_value && ($gappoints > 0.0)) {
1536 $gappoints = 0.0;
1537 }
1538 }
1539 $points += $gappoints;
1540 $detailed[$gap_id] = ["points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0.0) ? true : false];
1541 array_push($solution_values_numeric, $value["value"]);
1542 break;
1544 if ($value["value"] >= 0.0) {
1545 for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1546 $answer = $this->gaps[$gap_id]->getItem($order);
1547 if ($value["value"] == $answer->getOrder()) {
1548 $answerpoints = $answer->getPoints();
1549 if (!$this->getIdenticalScoring()) {
1550 // check if the same solution value was already entered
1551 if ((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0.0)) {
1552 $answerpoints = 0.0;
1553 }
1554 }
1555 $points += $answerpoints;
1556 $detailed[$gap_id] = ["points" => $answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? true : false, "positive" => ($answerpoints > 0.0) ? true : false];
1557 array_push($solution_values_select, $answer->getAnswertext());
1558 }
1559 }
1560 }
1561 break;
1562 }
1563 }
1564 }
1565
1566 return $points;
1567 }
1568
1570 {
1571 $participant_session = $preview_session->getParticipantsSolution();
1572
1573 if (!is_array($participant_session)) {
1574 return 0.0;
1575 }
1576
1577 $user_solution = [];
1578
1579 foreach ($participant_session as $key => $val) {
1580 $user_solution[$key] = ['gap_id' => $key, 'value' => $val];
1581 }
1582
1583 $reached_points = $this->calculateReachedPointsForSolution($user_solution);
1584
1585 return $this->ensureNonNegativePoints($reached_points);
1586 }
1587
1588 public function fetchAnswerValueForGap($userSolution, $gapIndex): string
1589 {
1590 $answerValue = '';
1591
1592 foreach ($userSolution as $value1 => $value2) {
1593 if ($value1 == $gapIndex) {
1594 $answerValue = $value2;
1595 break;
1596 }
1597 }
1598
1599 return $answerValue;
1600 }
1601
1602 public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
1603 {
1604 $gap = $this->getGap($qIndex);
1605
1606 if ($gap->getType() != assClozeGap::TYPE_TEXT) {
1607 return false;
1608 }
1609
1610 foreach ($gap->getItems($this->randomGroup->dontShuffle()) as $item) {
1611 if ($item->getAnswertext() === $answerOptionValue) {
1612 return false;
1613 }
1614 }
1615
1616 return true;
1617 }
1618
1619 public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
1620 {
1621 $gap = $this->getGap($qIndex); /* @var assClozeGap $gap */
1622
1623 $item = new assAnswerCloze($answerOptionValue, $points);
1624 $item->setOrder($gap->getItemCount());
1625
1626 $gap->addItem($item);
1627 }
1628
1629 public function toLog(AdditionalInformationGenerator $additional_info): array
1630 {
1631 $result = [
1632 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
1633 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
1634 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
1635 AdditionalInformationGenerator::KEY_QUESTION_CLOZE_CLOZETEXT => $this->formatSAQuestion($this->getClozeText()),
1636 AdditionalInformationGenerator::KEY_QUESTION_SHUFFLE_ANSWER_OPTIONS => $additional_info
1637 ->getTrueFalseTagForBool($this->getShuffle()),
1638 AdditionalInformationGenerator::KEY_FEEDBACK => [
1639 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1640 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1641 ]
1642 ];
1643
1644 $gaps = [];
1645 foreach ($this->getGaps() as $gap_index => $gap) {
1646 $items = [];
1647 foreach ($gap->getItems($this->getShuffler()) as $item) {
1648 $item_array = [
1649 AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $item->getPoints(),
1650 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => $this->formatSAQuestion($item->getAnswertext()),
1651 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION_ORDER => $item->getOrder()
1652 ];
1653 if ($gap->getType() === assClozeGap::TYPE_NUMERIC) {
1654 $item_array[AdditionalInformationGenerator::KEY_QUESTION_LOWER_LIMIT] = $item->getLowerBound();
1655 $item_array[AdditionalInformationGenerator::KEY_QUESTION_UPPER_LIMIT] = $item->getUpperBound();
1656 }
1657 array_push($items, $item_array);
1658 }
1659
1660 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_TEXTSIZE] = $gap->getGapSize();
1661 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_SHUFFLE_ANSWER_OPTIONS] = $additional_info->getTrueFalseTagForBool(
1662 $gap->getShuffle()
1663 );
1664 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_CLOZE_GAP_TYPE] = $gap->getType();
1665 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTIONS] = $items;
1666
1667 $gaps[$gap_index + 1] = $gap_array;
1668 }
1669 $result[AdditionalInformationGenerator::KEY_QUESTION_CLOZE_GAPS] = $gaps;
1670 return $result;
1671 }
1672
1673 protected function solutionValuesToLog(
1674 AdditionalInformationGenerator $additional_info,
1675 array $solution_values
1676 ): array {
1677 $parsed_solution = [];
1678 foreach ($this->getGaps() as $gap_index => $gap) {
1679 foreach ($solution_values as $solutionvalue) {
1680 if ($gap_index !== (int) $solutionvalue['value1']) {
1681 continue;
1682 }
1683
1684 if ($gap->getType() === assClozeGap::TYPE_SELECT) {
1685 $parsed_solution[$gap_index + 1] = $gap->getItem($solutionvalue['value2'])->getAnswertext();
1686 continue;
1687 }
1688
1689 $parsed_solution[$gap_index + 1] = $solutionvalue['value2'];
1690 }
1691 }
1692 return $parsed_solution;
1693 }
1694
1695 public function solutionValuesToText(array $solution_values): array
1696 {
1697 $parsed_solution = [];
1698 foreach ($this->getGaps() as $gap_index => $gap) {
1699 foreach ($solution_values as $solutionvalue) {
1700 if ($gap_index !== (int) $solutionvalue['value1']) {
1701 continue;
1702 }
1703
1704 if ($gap->getType() === assClozeGap::TYPE_SELECT) {
1705 $parsed_solution[] = $this->lng->txt('gap') . ' ' . $gap_index + 1 . ': '
1706 . $gap->getItem($solutionvalue['value2'])->getAnswertext();
1707 continue;
1708 }
1709
1710 $parsed_solution[] = $this->lng->txt('gap') . ' ' . $gap_index + 1 . ': '
1711 . $solutionvalue['value2'];
1712 }
1713 }
1714 return $parsed_solution;
1715 }
1716
1717 public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
1718 {
1719 $answers = [];
1720 foreach ($this->getGaps() as $gap_index => $gap) {
1721 $correct_answers = array_map(
1722 fn(int $v): string => $gap->getItem($v)->getAnswertext(),
1724 );
1725 $answers[] = $this->lng->txt('gap') . ' ' . $gap_index + 1 . ': '
1726 . implode(',', $correct_answers);
1727 }
1728 return $answers;
1729 }
1730}
setOrder($order=0)
Sets the order.
getOrder()
Gets the sort/display order.
getPoints()
Gets the points.
getAnswertext()
Gets the answer text.
return true
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getLowerBound()
Returns the lower bound.
getUpperBound()
Returns the upper bound.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Class for cloze question gaps.
const TEXTGAP_RATING_CASESENSITIVE
const TEXTGAP_RATING_LEVENSHTEIN1
const TEXTGAP_RATING_LEVENSHTEIN5
getBestSolutionIndexes()
Returns the indexes of the best solutions for the gap.
const TEXTGAP_RATING_CASEINSENSITIVE
const TEXTGAP_RATING_LEVENSHTEIN4
getItemCount()
Gets the item count.
getShuffle()
Gets the shuffle state of the items.
const TEXTGAP_RATING_LEVENSHTEIN2
const TEXTGAP_RATING_LEVENSHTEIN3
getItem($a_index)
Gets the item with a given index.
getItemsRaw()
Gets the items of a cloze gap.
addItem($a_item)
Adds a gap item.
getItems(Transformation $shuffler, ?int $gap_index=null)
Class for cloze tests.
clearGapAnswers()
Removes all answers from the gaps.
getEndTag()
Returns the end tag of a cloze gap.
getCorrectSolutionForTextOutput(int $active_id, int $pass)
addGapAnswer($gap_index, $order, $answer)
Sets the answer text of a gap with a given index.
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.
replaceFirstGap(string $gaptext, string $content)
setGapSize($gap_index, $size)
saveClozeSelectGapRecordToDb(int $next_id, int $key, assAnswerCloze $item, assClozeGap $gap)
getClozeText()
Returns the cloze text.
setIdenticalScoring(bool $identical_scoring)
Sets the identical scoring option for cloze questions.
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...
string $textgap_rating
The rating option for text gaps.
saveClozeGapItemsToDb(assClozeGap $gap, int $key)
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
getUserQuestionResult(int $active_id, int $pass)
Get the user solution for a question by active_id and the test pass.
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)
getGap(int $gap_index=0)
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 ...
setFixedTextLength(?int $fixed_text_length)
Sets a fixed text length for all text fields in the cloze question.
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.
saveClozeTextGapRecordToDb(int $next_id, int $key, assAnswerCloze $item, assClozeGap $gap)
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
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.
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
RandomGroup $randomGroup
updateClozeTextFromGaps()
Updates the gap parameters in the cloze text from the form input.
getStartTag()
Returns the start tag of a cloze gap.
solutionValuesToLog(AdditionalInformationGenerator $additional_info, array $solution_values)
MUST convert the given solution values into an array or a string that can be stored in the log.
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
getFixedTextLength()
Gets the fixed text length for all text fields in the cloze question.
__construct(string $title="", string $comment="", string $author="", int $owner=-1, string $question="")
ilAssQuestionFeedback $feedbackOBJ
addGapText($gap_index)
Adds a new answer text value to a text gap with a given index.
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
saveClozeNumericGapRecordToDb(int $next_id, int $key, assAnswerCloze $item, assClozeGap $gap)
getOperators(string $expression)
Get all available operations for a specific question.
getQuestionType()
Returns the question type of the question.
setClozeText(string $cloze_text='')
getTextgapRating()
Returns the rating option for text gaps.
deleteGap($gap_index)
Deletes a gap with a given index.
setGapType($gap_index, $gap_type)
Set the type of a gap with a given index.
getExpressionTypes()
Get all available expression types for a specific question.
checkForValidFormula(string $value)
getGapCount()
Returns the number of gaps.
setGapCombinationsExists($value)
getUserResultDetails(int $active_id, ?int $pass=null, bool $authorized_solution=true)
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 a record to the question types additional data table.
bool $identical_scoring
Defines the scoring for "identical solutions".
deleteAnswerText($gap_index, $answer_index)
Deletes the answer text of a gap with a given index and an answer with a given order.
toJSON()
Returns a JSON representation of the question.
calculateReachedPointsForSolution(?array $user_result, array &$detailed=[])
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
saveToDb(?int $original_id=null)
calculateCombinationResult($user_result)
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
setStartTag($start_tag="[gap]")
Sets the start tag of a cloze gap.
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
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.
loadFromDb(int $question_id)
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 ...
fetchUserResult(int $active_id, ?int $pass)
cloneQuestionTypeSpecificProperties(\assQuestion $target)
saveToDb(?int $original_id=null)
setQuestion(string $question="")
const FB_MODE_GAP_QUESTION
constants for different feedback modes (per gap or per gap-answers/options)
Class ilDBConstants.
static prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free,...
static _getMobsOfObject(string $a_type, int $a_id, int|false $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 getOperatorsByExpression(string $expression)
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...
ILIAS Setting Class.
static strToLower(string $a_string)
Definition: class.ilStr.php:69
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...
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...
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$text
Definition: xapiexit.php:21