ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
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 'integer',
315 'integer',
316 'integer',
317 'text',
318 'float',
319 'integer',
320 'text',
321 'integer'
322 ],
323 [
324 $next_id,
325 $this->getId(),
326 $key,
327 strlen($item->getAnswertext()) ? $item->getAnswertext() : '',
328 $item->getPoints(),
329 $item->getOrder(),
330 $gap->getType(),
331 (int) $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 'integer',
346 'integer',
347 'integer',
348 'text',
349 'float',
350 'integer',
351 'text',
352 'text'
353 ],
354 [
355 $next_id,
356 $this->getId(),
357 $key,
358 strlen($item->getAnswertext()) ? $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 [
378 'integer',
379 'integer',
380 'integer',
381 'text',
382 'float',
383 'integer',
384 'text',
385 'text',
386 'text',
387 'integer'
388 ],
389 [
390 $next_id,
391 $this->getId(),
392 $key,
393 strlen($item->getAnswertext()) ? $item->getAnswertext() : '',
394 $item->getPoints(),
395 $item->getOrder(),
396 $gap->getType(),
397 ($eval->e($item->getLowerBound()) !== false && strlen(
398 $item->getLowerBound()
399 ) > 0) ? $item->getLowerBound() : $item->getAnswertext(),
400 ($eval->e($item->getUpperBound()) !== false && strlen(
401 $item->getUpperBound()
402 ) > 0) ? $item->getUpperBound() : $item->getAnswertext(),
403 (int) $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 return $target;
796 }
797
803 public function updateClozeTextFromGaps(): void
804 {
805 $output = $this->getClozeText();
806 foreach ($this->getGaps() as $gap_index => $gap) {
807 $answers = [];
808 foreach ($gap->getItemsRaw() as $item) {
809 array_push($answers, str_replace([',', '['], ["\\,", '[&hairsp;'], $item->getAnswerText()));
810 }
811 // fau: fixGapReplace - use replace function
812 $output = $this->replaceFirstGap($output, "[_gap]" . ilLegacyFormElementsUtil::prepareTextareaOutput(join(",", $answers), true) . "[/_gap]");
813 // fau.
814 }
815 $output = str_replace("_gap]", "gap]", $output);
816 $this->cloze_text = $output;
817 }
818
828 public function deleteAnswerText($gap_index, $answer_index): void
829 {
830 if (array_key_exists($gap_index, $this->gaps)) {
831 if ($this->gaps[$gap_index]->getItemCount() == 1) {
832 // this is the last answer text => remove the gap
833 $this->deleteGap($gap_index);
834 } else {
835 // remove the answer text
836 $this->gaps[$gap_index]->deleteItem($answer_index);
837 $this->updateClozeTextFromGaps();
838 }
839 }
840 }
841
850 public function deleteGap($gap_index): void
851 {
852 if (array_key_exists($gap_index, $this->gaps)) {
853 $output = $this->getClozeText();
854 foreach ($this->getGaps() as $replace_gap_index => $gap) {
855 $answers = [];
856 foreach ($gap->getItemsRaw() as $item) {
857 array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
858 }
859 if ($replace_gap_index == $gap_index) {
860 // fau: fixGapReplace - use replace function
861 $output = $this->replaceFirstGap($output, '');
862 // fau.
863 } else {
864 // fau: fixGapReplace - use replace function
865 $output = $this->replaceFirstGap($output, "[_gap]" . join(",", $answers) . "[/_gap]");
866 // fau.
867 }
868 }
869 $output = str_replace("_gap]", "gap]", $output);
870 $this->cloze_text = $output;
871 unset($this->gaps[$gap_index]);
872 $this->gaps = array_values($this->gaps);
873 }
874 }
875
885 public function getTextgapPoints($a_original, $a_entered, $max_points): float
886 {
887 global $DIC;
888 $refinery = $DIC->refinery();
889 $result = 0;
890 $gaprating = $this->getTextgapRating();
891
892 switch ($gaprating) {
894 if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) {
895 $result = $max_points;
896 }
897 break;
899 if (strcmp($a_original, $a_entered) == 0) {
900 $result = $max_points;
901 }
902 break;
904 $transformation = $refinery->string()->levenshtein()->standard($a_original, 1);
905 break;
907 $transformation = $refinery->string()->levenshtein()->standard($a_original, 2);
908 break;
910 $transformation = $refinery->string()->levenshtein()->standard($a_original, 3);
911 break;
913 $transformation = $refinery->string()->levenshtein()->standard($a_original, 4);
914 break;
916 $transformation = $refinery->string()->levenshtein()->standard($a_original, 5);
917 break;
918 }
919
920 // run answers against Levenshtein2 methods
921 if (isset($transformation) && $transformation->transform($a_entered) >= 0) {
922 $result = $max_points;
923 }
924 return $result;
925 }
926
927
937 public function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound): float
938 {
939 $eval = new EvalMath();
940 $eval->suppress_errors = true;
941 $result = 0.0;
942
943 if ($eval->e($a_entered) === false) {
944 return 0.0;
945 } elseif (($eval->e($lowerBound) !== false) && ($eval->e($upperBound) !== false)) {
946 if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
947 $result = $max_points;
948 }
949 } elseif ($eval->e($lowerBound) !== false) {
950 if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) {
951 $result = $max_points;
952 }
953 } elseif ($eval->e($upperBound) !== false) {
954 if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
955 $result = $max_points;
956 }
957 } elseif ($eval->e($a_entered) == $eval->e($a_original)) {
958 $result = $max_points;
959 }
960 return $result;
961 }
962
963 public function checkForValidFormula(string $value): int
964 {
965 return preg_match("/^-?(\\d*)(,|\\.|\\/){0,1}(\\d*)$/", $value, $matches);
966 }
967
968 public function calculateReachedPoints(
969 int $active_id,
970 ?int $pass = null,
971 bool $authorized_solution = true
972 ): float {
973 $user_result = $this->fetchUserResult($active_id, $pass, $authorized_solution);
974 return $this->calculateReachedPointsForSolution($user_result);
975 }
976
977 public function getUserResultDetails(
978 int $active_id,
979 ?int $pass = null,
980 bool $authorized_solution = true
981 ): array {
982 $user_result = $this->fetchUserResult($active_id, $pass, $authorized_solution);
983 $detailed = [];
984 $this->calculateReachedPointsForSolution($user_result, $detailed);
985 return $detailed;
986 }
987
988 private function fetchUserResult(
989 int $active_id,
990 ?int $pass
991 ): array {
992 if (is_null($pass)) {
993 $pass = $this->getSolutionMaxPass($active_id);
994 }
995
996 $result = $this->getCurrentSolutionResultSet($active_id, $pass, true);
997 $user_result = [];
998 while ($data = $this->db->fetchAssoc($result)) {
999 if ($data['value2'] === '') {
1000 continue;
1001 }
1002 $user_result[$data['value1']] = [
1003 'gap_id' => $data['value1'],
1004 'value' => $data['value2']
1005 ];
1006 }
1007
1008 ksort($user_result);
1009 return $user_result;
1010 }
1011
1012 protected function isValidNumericSubmitValue($submittedValue): bool
1013 {
1014 if (is_numeric($submittedValue)) {
1015 return true;
1016 }
1017
1018 if (preg_match('/^[-+]{0,1}\d+\/\d+$/', $submittedValue)) {
1019 return true;
1020 }
1021
1022 return false;
1023 }
1024
1025 public function fetchSolutionSubmit(): array
1026 {
1027 $solution_submit = [];
1028 $post_wrapper = $this->dic->http()->wrapper()->post();
1029 foreach ($this->getGaps() as $index => $gap) {
1030 if (!$post_wrapper->has("gap_$index")) {
1031 continue;
1032 }
1033 $value = trim($post_wrapper->retrieve(
1034 "gap_$index",
1035 $this->dic->refinery()->kindlyTo()->string()
1036 ));
1037 if ($value === '') {
1038 continue;
1039 }
1040
1041 if ($gap->getType() === assClozeGap::TYPE_SELECT && $value === '-1') {
1042 continue;
1043 }
1044
1045 if ($gap->getType() === assClozeGap::TYPE_NUMERIC) {
1046 $value = str_replace(',', '.', $value);
1047 if (!is_numeric($value)) {
1048 $value = null;
1049 }
1050 }
1051
1052 $solution_submit[$index] = $value;
1053 }
1054
1055 return $solution_submit;
1056 }
1057
1058 protected function getSolutionSubmit(): array
1059 {
1060 return $this->fetchSolutionSubmit();
1061 }
1062
1063 public function saveWorkingData(
1064 int $active_id,
1065 ?int $pass = null,
1066 bool $authorized = true
1067 ): bool {
1068 if (is_null($pass)) {
1069 $pass = ilObjTest::_getPass($active_id);
1070 }
1071
1072 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
1073 function () use ($active_id, $pass, $authorized) {
1074 $this->removeCurrentSolution($active_id, $pass, $authorized);
1075
1076 foreach ($this->fetchSolutionSubmit() as $key => $value) {
1077 if ($value === null || $value === '') {
1078 continue;
1079 }
1080 $gap = $this->getGap($key);
1081 if ($gap === null
1082 || $gap->getType() === assClozeGap::TYPE_SELECT && $value === -1) {
1083 continue;
1084 }
1085 $this->saveCurrentSolution($active_id, $pass, $key, $value, $authorized);
1086 }
1087 }
1088 );
1089
1090 return true;
1091 }
1092
1099 public function getQuestionType(): string
1100 {
1101 return "assClozeTest";
1102 }
1103
1111 public function getTextgapRating(): string
1112 {
1113 return $this->textgap_rating;
1114 }
1115
1123 public function setTextgapRating($a_textgap_rating): void
1124 {
1125 switch ($a_textgap_rating) {
1133 $this->textgap_rating = $a_textgap_rating;
1134 break;
1135 default:
1136 $this->textgap_rating = assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE;
1137 break;
1138 }
1139 }
1140
1148 public function getIdenticalScoring(): bool
1149 {
1150 return $this->identical_scoring;
1151 }
1152
1160 public function setIdenticalScoring(bool $identical_scoring): void
1161 {
1162 $this->identical_scoring = $identical_scoring;
1163 }
1164
1171 public function getAdditionalTableName(): string
1172 {
1173 return "qpl_qst_cloze";
1174 }
1175
1176 public function getAnswerTableName(): array
1177 {
1178 return ["qpl_a_cloze",'qpl_a_cloze_combi_res'];
1179 }
1180
1187 public function setFixedTextLength(?int $fixed_text_length): void
1188 {
1189 $this->fixed_text_length = $fixed_text_length;
1190 }
1191
1198 public function getFixedTextLength(): ?int
1199 {
1200 return $this->fixed_text_length;
1201 }
1202
1211 public function getMaximumGapPoints($gap_index)
1212 {
1213 $points = 0;
1214 $gap_max_points = 0;
1215 if (array_key_exists($gap_index, $this->gaps)) {
1216 $gap = &$this->gaps[$gap_index];
1217 foreach ($gap->getItems($this->getShuffler()) as $answer) {
1218 if ($answer->getPoints() > $gap_max_points) {
1219 $gap_max_points = $answer->getPoints();
1220 }
1221 }
1222 $points += $gap_max_points;
1223 }
1224 return $points;
1225 }
1226
1231 public function getRTETextWithMediaObjects(): string
1232 {
1233 return parent::getRTETextWithMediaObjects() . $this->getClozeText();
1234 }
1235 public function getGapCombinationsExists(): bool
1236 {
1237 return $this->gap_combinations_exist;
1238 }
1239
1240 public function getGapCombinations(): array
1241 {
1242 return $this->gap_combinations;
1243 }
1244
1245 public function setGapCombinationsExists($value): void
1246 {
1247 $this->gap_combinations_exist = $value;
1248 }
1249
1250 public function setGapCombinations($value): void
1251 {
1252 $this->gap_combinations = $value;
1253 }
1254
1259 {
1260 // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1261 //$this->setClozeText( $migrator->migrateToLmContent($this->getClozeText()) );
1262 $this->cloze_text = $migrator->migrateToLmContent($this->getClozeText());
1263 // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1264 }
1265
1269 public function toJSON(): string
1270 {
1271 $result = [
1272 'id' => $this->getId(),
1273 'type' => (string) $this->getQuestionType(),
1274 'title' => $this->getTitleForHTMLOutput(),
1275 'question' => $this->formatSAQuestion($this->getQuestion()),
1276 'clozetext' => $this->formatSAQuestion($this->getClozeText()),
1277 'nr_of_tries' => $this->getNrOfTries(),
1278 'shuffle' => $this->getShuffle(),
1279 'feedback' => [
1280 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1281 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1282 ]
1283 ];
1284
1285 $gaps = [];
1286 foreach ($this->getGaps() as $key => $gap) {
1287 $items = [];
1288 foreach ($gap->getItems($this->getShuffler()) as $item) {
1289 $jitem = [];
1290 $jitem['points'] = $item->getPoints();
1291 $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1292 $jitem['order'] = $item->getOrder();
1293 if ($gap->getType() == assClozeGap::TYPE_NUMERIC) {
1294 $jitem['lowerbound'] = $item->getLowerBound();
1295 $jitem['upperbound'] = $item->getUpperBound();
1296 } else {
1297 $jitem['value'] = trim($jitem['value']);
1298 }
1299 array_push($items, $jitem);
1300 }
1301
1302 if ($gap->getGapSize() && ($gap->getType() == assClozeGap::TYPE_TEXT || $gap->getType() == assClozeGap::TYPE_NUMERIC)) {
1303 $jgap['size'] = $gap->getGapSize();
1304 }
1305
1306 $jgap['shuffle'] = $gap->getShuffle();
1307 $jgap['type'] = $gap->getType();
1308 $jgap['item'] = $items;
1309
1310 array_push($gaps, $jgap);
1311 }
1312 $result['gaps'] = $gaps;
1313 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1314 $result['mobs'] = $mobs;
1315 return json_encode($result);
1316 }
1317
1318 public function getOperators(string $expression): array
1319 {
1321 }
1322
1323 public function getExpressionTypes(): array
1324 {
1325 return [
1331 ];
1332 }
1333
1334 public function getUserQuestionResult(
1335 int $active_id,
1336 int $pass
1338 $result = new ilUserQuestionResult($this, $active_id, $pass);
1339
1340 $maxStep = $this->lookupMaxStep($active_id, $pass);
1341 if ($maxStep > 0) {
1342 $data = $this->db->queryF(
1343 "
1344 SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1345 FROM tst_solutions sol
1346 INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1347 WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s AND sol.step = %s
1348 GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1349 ",
1350 ["integer", "integer", "integer","integer"],
1351 [$active_id, $pass, $this->getId(), $maxStep]
1352 );
1353 } else {
1354 $data = $this->db->queryF(
1355 "
1356 SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1357 FROM tst_solutions sol
1358 INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1359 WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s
1360 GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1361 ",
1362 ["integer", "integer", "integer"],
1363 [$active_id, $pass, $this->getId()]
1364 );
1365 }
1366
1367 while ($row = $this->db->fetchAssoc($data)) {
1368 if ($row["cloze_type"] == 1) {
1369 $row["value2"]++;
1370 }
1371 $result->addKeyValue($row["val"], $row["value2"]);
1372 }
1373
1374 $points = $this->calculateReachedPoints($active_id, $pass);
1375 $max_points = $this->getMaximumPoints();
1376
1377 $result->setReachedPercentage(($points / $max_points) * 100);
1378
1379 return $result;
1380 }
1381
1390 public function getAvailableAnswerOptions($index = null)
1391 {
1392 if ($index !== null) {
1393 return $this->getGap($index);
1394 } else {
1395 return $this->getGaps();
1396 }
1397 }
1398
1399 public function calculateCombinationResult($user_result): array
1400 {
1401 $points = 0;
1402
1403 $assClozeGapCombinationObj = new assClozeGapCombination($this->db);
1404 $gap_used_in_combination = [];
1405 if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1406 $combinations_for_question = $assClozeGapCombinationObj->getCleanCombinationArray($this->getId());
1407 $gap_answers = [];
1408
1409 foreach ($user_result as $user_result_build_list) {
1410 if (is_array($user_result_build_list)) {
1411 $gap_answers[$user_result_build_list['gap_id']] = $user_result_build_list['value'];
1412 }
1413 }
1414
1415 foreach ($combinations_for_question as $combination) {
1416 foreach ($combination as $row_key => $row_answers) {
1417 $combination_fulfilled = true;
1418 $points_for_combination = $row_answers['points'];
1419 foreach ($row_answers as $gap_key => $combination_gap_answer) {
1420 if ($gap_key !== 'points') {
1421 $gap_used_in_combination[$gap_key] = $gap_key;
1422 }
1423 if ($combination_fulfilled && array_key_exists($gap_key, $gap_answers)) {
1424 switch ($combination_gap_answer['type']) {
1426 $is_text_gap_correct = $this->getTextgapPoints($gap_answers[$gap_key], $combination_gap_answer['answer'], 1);
1427 if ($is_text_gap_correct != 1) {
1428 $combination_fulfilled = false;
1429 }
1430 break;
1432 $answer = $this->gaps[$gap_key]->getItem($gap_answers[$gap_key]);
1433 $answertext = $answer?->getAnswertext();
1434 if ($answertext != $combination_gap_answer['answer']) {
1435 $combination_fulfilled = false;
1436 }
1437 break;
1439 $answer = $this->gaps[$gap_key]->getItem(0);
1440 if ($combination_gap_answer['answer'] != 'out_of_bound') {
1441 $is_numeric_gap_correct = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1442 if ($is_numeric_gap_correct != 1) {
1443 $combination_fulfilled = false;
1444 }
1445 } else {
1446 $wrong_is_the_new_right = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1447 if ($wrong_is_the_new_right == 1) {
1448 $combination_fulfilled = false;
1449 }
1450 }
1451 break;
1452 }
1453 } else {
1454 if ($gap_key !== 'points') {
1455 $combination_fulfilled = false;
1456 }
1457 }
1458 }
1459 if ($combination_fulfilled) {
1460 $points += $points_for_combination;
1461 }
1462 }
1463 }
1464 }
1465 return [$points, $gap_used_in_combination];
1466 }
1471 protected function calculateReachedPointsForSolution(?array $user_result, array &$detailed = []): float
1472 {
1473 $points = 0.0;
1474
1475 $assClozeGapCombinationObj = new assClozeGapCombination($this->db);
1476 $combinations[1] = [];
1477 if ($this->gap_combinations_exist) {
1478 $combinations = $this->calculateCombinationResult($user_result);
1479 $points = $combinations[0];
1480 }
1481
1482 $solution_values_text = []; // for identical scoring checks
1483 $solution_values_select = []; // for identical scoring checks
1484 $solution_values_numeric = []; // for identical scoring checks
1485 foreach ($user_result as $gap_id => $value) {
1486 if (is_string($value)) {
1487 $value = ["value" => $value];
1488 }
1489
1490 if (array_key_exists($gap_id, $this->gaps) && !array_key_exists($gap_id, $combinations[1])) {
1491 switch ($this->gaps[$gap_id]->getType()) {
1493 $gappoints = 0.0;
1494 for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1495 $answer = $this->gaps[$gap_id]->getItem($order);
1496 $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1497 if ($gotpoints > $gappoints) {
1498 $gappoints = $gotpoints;
1499 }
1500 }
1501 if (!$this->getIdenticalScoring()) {
1502 // check if the same solution text was already entered
1503 if ((in_array($value["value"], $solution_values_text)) && ($gappoints > 0.0)) {
1504 $gappoints = 0.0;
1505 }
1506 }
1507 $points += $gappoints;
1508 $detailed[$gap_id] = ["points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0.0) ? true : false];
1509 array_push($solution_values_text, $value["value"]);
1510 break;
1512 $gappoints = 0.0;
1513 for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1514 $answer = $this->gaps[$gap_id]->getItem($order);
1515 $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1516 if ($gotpoints > $gappoints) {
1517 $gappoints = $gotpoints;
1518 }
1519 }
1520 if (!$this->getIdenticalScoring()) {
1521 // check if the same solution value was already entered
1522 $eval = new EvalMath();
1523 $eval->suppress_errors = true;
1524 $found_value = false;
1525 foreach ($solution_values_numeric as $solval) {
1526 if ($eval->e($solval) == $eval->e($value["value"])) {
1527 $found_value = true;
1528 }
1529 }
1530 if ($found_value && ($gappoints > 0.0)) {
1531 $gappoints = 0.0;
1532 }
1533 }
1534 $points += $gappoints;
1535 $detailed[$gap_id] = ["points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0.0) ? true : false];
1536 array_push($solution_values_numeric, $value["value"]);
1537 break;
1539 if ($value["value"] >= 0.0) {
1540 for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1541 $answer = $this->gaps[$gap_id]->getItem($order);
1542 if ($value["value"] == $answer->getOrder()) {
1543 $answerpoints = $answer->getPoints();
1544 if (!$this->getIdenticalScoring()) {
1545 // check if the same solution value was already entered
1546 if ((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0.0)) {
1547 $answerpoints = 0.0;
1548 }
1549 }
1550 $points += $answerpoints;
1551 $detailed[$gap_id] = ["points" => $answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? true : false, "positive" => ($answerpoints > 0.0) ? true : false];
1552 array_push($solution_values_select, $answer->getAnswertext());
1553 }
1554 }
1555 }
1556 break;
1557 }
1558 }
1559 }
1560
1561 return $points;
1562 }
1563
1565 {
1566 $participant_session = $preview_session->getParticipantsSolution();
1567
1568 if (!is_array($participant_session)) {
1569 return 0.0;
1570 }
1571
1572 $user_solution = [];
1573
1574 foreach ($participant_session as $key => $val) {
1575 $user_solution[$key] = ['gap_id' => $key, 'value' => $val];
1576 }
1577
1578 $reached_points = $this->calculateReachedPointsForSolution($user_solution);
1579
1580 return $this->ensureNonNegativePoints($reached_points);
1581 }
1582
1583 public function fetchAnswerValueForGap($userSolution, $gapIndex): string
1584 {
1585 $answerValue = '';
1586
1587 foreach ($userSolution as $value1 => $value2) {
1588 if ($value1 == $gapIndex) {
1589 $answerValue = $value2;
1590 break;
1591 }
1592 }
1593
1594 return $answerValue;
1595 }
1596
1597 public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
1598 {
1599 $gap = $this->getGap($qIndex);
1600
1601 if ($gap->getType() != assClozeGap::TYPE_TEXT) {
1602 return false;
1603 }
1604
1605 foreach ($gap->getItems($this->randomGroup->dontShuffle()) as $item) {
1606 if ($item->getAnswertext() === $answerOptionValue) {
1607 return false;
1608 }
1609 }
1610
1611 return true;
1612 }
1613
1614 public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
1615 {
1616 $gap = $this->getGap($qIndex); /* @var assClozeGap $gap */
1617
1618 $item = new assAnswerCloze($answerOptionValue, $points);
1619 $item->setOrder($gap->getItemCount());
1620
1621 $gap->addItem($item);
1622 }
1623
1624 public function toLog(AdditionalInformationGenerator $additional_info): array
1625 {
1626 $result = [
1627 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
1628 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
1629 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
1630 AdditionalInformationGenerator::KEY_QUESTION_CLOZE_CLOZETEXT => $this->formatSAQuestion($this->getClozeText()),
1631 AdditionalInformationGenerator::KEY_QUESTION_SHUFFLE_ANSWER_OPTIONS => $additional_info
1632 ->getTrueFalseTagForBool($this->getShuffle()),
1633 AdditionalInformationGenerator::KEY_FEEDBACK => [
1634 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1635 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1636 ]
1637 ];
1638
1639 $gaps = [];
1640 foreach ($this->getGaps() as $gap_index => $gap) {
1641 $items = [];
1642 foreach ($gap->getItems($this->getShuffler()) as $item) {
1643 $item_array = [
1644 AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $item->getPoints(),
1645 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => $this->formatSAQuestion($item->getAnswertext()),
1646 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION_ORDER => $item->getOrder()
1647 ];
1648 if ($gap->getType() === assClozeGap::TYPE_NUMERIC) {
1649 $item_array[AdditionalInformationGenerator::KEY_QUESTION_LOWER_LIMIT] = $item->getLowerBound();
1650 $item_array[AdditionalInformationGenerator::KEY_QUESTION_UPPER_LIMIT] = $item->getUpperBound();
1651 }
1652 array_push($items, $item_array);
1653 }
1654
1655 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_TEXTSIZE] = $gap->getGapSize();
1656 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_SHUFFLE_ANSWER_OPTIONS] = $additional_info->getTrueFalseTagForBool(
1657 $gap->getShuffle()
1658 );
1659 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_CLOZE_GAP_TYPE] = $gap->getType();
1660 $gap_array[AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTIONS] = $items;
1661
1662 $gaps[$gap_index + 1] = $gap_array;
1663 }
1664 $result[AdditionalInformationGenerator::KEY_QUESTION_CLOZE_GAPS] = $gaps;
1665 return $result;
1666 }
1667
1668 protected function solutionValuesToLog(
1669 AdditionalInformationGenerator $additional_info,
1670 array $solution_values
1671 ): array {
1672 $parsed_solution = [];
1673 foreach ($this->getGaps() as $gap_index => $gap) {
1674 foreach ($solution_values as $solutionvalue) {
1675 if ($gap_index !== (int) $solutionvalue['value1']) {
1676 continue;
1677 }
1678
1679 if ($gap->getType() === assClozeGap::TYPE_SELECT) {
1680 $parsed_solution[$gap_index + 1] = $gap->getItem($solutionvalue['value2'])->getAnswertext();
1681 continue;
1682 }
1683
1684 $parsed_solution[$gap_index + 1] = $solutionvalue['value2'];
1685 }
1686 }
1687 return $parsed_solution;
1688 }
1689
1690 public function solutionValuesToText(array $solution_values): array
1691 {
1692 $parsed_solution = [];
1693 foreach ($this->getGaps() as $gap_index => $gap) {
1694 foreach ($solution_values as $solutionvalue) {
1695 if ($gap_index !== (int) $solutionvalue['value1']) {
1696 continue;
1697 }
1698
1699 if ($gap->getType() === assClozeGap::TYPE_SELECT) {
1700 $parsed_solution[] = $this->lng->txt('gap') . ' ' . $gap_index + 1 . ': '
1701 . $gap->getItem($solutionvalue['value2'])->getAnswertext();
1702 continue;
1703 }
1704
1705 $parsed_solution[] = $this->lng->txt('gap') . ' ' . $gap_index + 1 . ': '
1706 . $solutionvalue['value2'];
1707 }
1708 }
1709 return $parsed_solution;
1710 }
1711
1712 public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
1713 {
1714 $answers = [];
1715 foreach ($this->getGaps() as $gap_index => $gap) {
1716 $correct_answers = array_map(
1717 fn(int $v): string => $gap->getItem($v)->getAnswertext(),
1719 );
1720 $answers[] = $this->lng->txt('gap') . ' ' . $gap_index + 1 . ': '
1721 . implode(',', $correct_answers);
1722 }
1723 return $answers;
1724 }
1725}
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)
setQuestion(string $question="")
const FB_MODE_GAP_QUESTION
constants for different feedback modes (per gap or per gap-answers/options)
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 $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