ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.assTextSubset.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
24
41{
42 public array $answers = [];
43 public int $correctanswers = 0;
45
46 public function isComplete(): bool
47 {
48 if (
49 strlen($this->title)
50 && $this->author
51 && $this->question &&
52 count($this->answers) >= $this->correctanswers
53 && $this->getMaximumPoints() > 0
54 ) {
55 return true;
56 }
57 return false;
58 }
59
60 public function saveToDb(?int $original_id = null): void
61 {
65
66 parent::saveToDb();
67 }
68
69 public function loadFromDb(int $question_id): void
70 {
71 $result = $this->db->queryF(
72 "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",
73 ["integer"],
74 [$question_id]
75 );
76 if ($result->numRows() == 1) {
77 $data = $this->db->fetchAssoc($result);
78 $this->setId($question_id);
79 $this->setObjId($data['obj_fi']);
80 $this->setNrOfTries($data['nr_of_tries']);
81 $this->setTitle((string) $data['title']);
82 $this->setComment((string) $data['description']);
83 $this->setOriginalId($data['original_id']);
84 $this->setAuthor($data['author']);
85 $this->setPoints($data['points']);
86 $this->setOwner($data['owner']);
87 $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc((string) $data['question_text'], 1));
88 $this->setCorrectAnswers((int) $data['correctanswers']);
89 $this->setTextRating($data['textgap_rating'] ?? assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE);
90
91 try {
92 $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
95 }
96
97 try {
98 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
100 }
101 }
102
103
104 $result = $this->db->queryF(
105 "SELECT * FROM qpl_a_textsubset WHERE question_fi = %s ORDER BY aorder ASC",
106 ['integer'],
107 [$question_id]
108 );
109 if ($result->numRows() > 0) {
110 while ($data = $this->db->fetchAssoc($result)) {
111 $this->answers[] = new ASS_AnswerBinaryStateImage($data["answertext"], $data["points"], $data["aorder"]);
112 }
113 }
114
115 parent::loadFromDb($question_id);
116 }
117
123 public function addAnswer($answertext, $points, $order): void
124 {
125 if (array_key_exists($order, $this->answers)) {
126 // insert answer
127 $answer = new ASS_AnswerBinaryStateImage($answertext, $points, $order);
128 $newchoices = [];
129 for ($i = 0; $i < $order; $i++) {
130 $newchoices[] = $this->answers[$i];
131 }
132 $newchoices[] = $answer;
133 for ($i = $order, $iMax = count($this->answers); $i < $iMax; $i++) {
134 $changed = $this->answers[$i];
135 $changed->setOrder($i + 1);
136 $newchoices[] = $changed;
137 }
138 $this->answers = $newchoices;
139 } else {
140 // add answer
141 $this->answers[] = new ASS_AnswerBinaryStateImage($answertext, $points, count($this->answers));
142 }
143 }
144
152 public function getAnswerCount(): int
153 {
154 return count($this->answers);
155 }
156
166 public function getAnswer($index = 0): ?object
167 {
168 if ($index < 0) {
169 return null;
170 }
171 if (count($this->answers) < 1) {
172 return null;
173 }
174 if ($index >= count($this->answers)) {
175 return null;
176 }
177
178 return $this->answers[$index];
179 }
180
189 public function deleteAnswer($index = 0): void
190 {
191 if ($index < 0) {
192 return;
193 }
194 if (count($this->answers) < 1) {
195 return;
196 }
197 if ($index >= count($this->answers)) {
198 return;
199 }
200 unset($this->answers[$index]);
201 $this->answers = array_values($this->answers);
202 for ($i = 0, $iMax = count($this->answers); $i < $iMax; $i++) {
203 if ($this->answers[$i]->getOrder() > $index) {
204 $this->answers[$i]->setOrder($i);
205 }
206 }
207 }
208
215 public function flushAnswers(): void
216 {
217 $this->answers = [];
218 }
219
226 public function getMaximumPoints(): float
227 {
228 $points = [];
229 foreach ($this->answers as $answer) {
230 if ($answer->getPoints() > 0) {
231 $points[] = $answer->getPoints();
232 }
233 }
234 rsort($points, SORT_NUMERIC);
235 $maxpoints = 0;
236 for ($counter = 0; $counter < $this->getCorrectAnswers(); $counter++) {
237 if (isset($points[$counter])) {
238 $maxpoints += $points[$counter];
239 }
240 }
241 return $maxpoints;
242 }
243
250 public function getAvailableAnswers(): array
251 {
252 $available_answers = [];
253 foreach ($this->answers as $answer) {
254 $available_answers[] = $answer->getAnswertext();
255 }
256 return $available_answers;
257 }
258
269 public function isAnswerCorrect($answers, $answer)
270 {
271 global $DIC;
272 $refinery = $DIC->refinery();
273 $textrating = $this->getTextRating();
274
275 foreach ($answers as $key => $value) {
276 if ($this->answers[$key]->getPoints() <= 0) {
277 continue;
278 }
279 $value = html_entity_decode($value); #SB
280 switch ($textrating) {
282 if (strcmp(ilStr::strToLower($value), ilStr::strToLower($answer)) == 0) { #SB
283 return $key;
284 }
285 break;
287 if (strcmp($value, $answer) == 0) {
288 return $key;
289 }
290 break;
292 $transformation = $refinery->string()->levenshtein()->standard($answer, 1);
293 break;
295 $transformation = $refinery->string()->levenshtein()->standard($answer, 2);
296 break;
298 $transformation = $refinery->string()->levenshtein()->standard($answer, 3);
299 break;
301 $transformation = $refinery->string()->levenshtein()->standard($answer, 4);
302 break;
304 $transformation = $refinery->string()->levenshtein()->standard($answer, 5);
305 break;
306 }
307
308 // run answers against Levenshtein2 methods
309 if (isset($transformation) && $transformation->transform($value) >= 0) {
310 return $key;
311 }
312 }
313 return false;
314 }
315
316 public function getTextRating(): string
317 {
318 return $this->text_rating;
319 }
320
321 public function setTextRating(string $text_rating): void
322 {
323 switch ($text_rating) {
331 $this->text_rating = $text_rating;
332 break;
333 default:
335 break;
336 }
337 }
338
339 public function calculateReachedPoints(
340 int $active_id,
341 ?int $pass = null,
342 bool $authorized_solution = true
343 ): float {
344 if ($pass === null) {
345 $pass = $this->getSolutionMaxPass($active_id);
346 }
347 $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorized_solution);
348
349 $enteredTexts = [];
350 while ($data = $this->db->fetchAssoc($result)) {
351 $enteredTexts[] = $data['value1'];
352 }
353
354 return $this->calculateReachedPointsForSolution($enteredTexts);
355 }
356
357 public function setCorrectAnswers(int $a_correct_answers): void
358 {
359 $this->correctanswers = $a_correct_answers;
360 }
361
362 public function getCorrectAnswers(): int
363 {
364 return $this->correctanswers;
365 }
366
367 public function saveWorkingData(
368 int $active_id,
369 ?int $pass = null,
370 bool $authorized = true
371 ): bool {
372 if ($pass === null) {
373 $pass = ilObjTest::_getPass($active_id);
374 }
375
376 $solution_submit = $this->getSolutionSubmit();
377 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
378 function () use ($active_id, $pass, $authorized, $solution_submit) {
379 $this->removeCurrentSolution($active_id, $pass, $authorized);
380
381 foreach ($solution_submit as $value) {
382 if ($value !== '') {
383 $this->saveCurrentSolution($active_id, $pass, $value, null, $authorized);
384 }
385 }
386 }
387 );
388
389 return true;
390 }
391
393 {
394 // save additional data
395 $this->db->manipulateF(
396 "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
397 [ "integer" ],
398 [ $this->getId() ]
399 );
400
401 $this->db->manipulateF(
402 "INSERT INTO " . $this->getAdditionalTableName(
403 ) . " (question_fi, textgap_rating, correctanswers) VALUES (%s, %s, %s)",
404 [ "integer", "text", "integer" ],
405 [
406 $this->getId(),
407 $this->getTextRating(),
408 $this->getCorrectAnswers()
409 ]
410 );
411 }
412
414 {
415 $this->db->manipulateF(
416 "DELETE FROM qpl_a_textsubset WHERE question_fi = %s",
417 [ 'integer' ],
418 [ $this->getId() ]
419 );
420
421 foreach ($this->answers as $key => $value) {
422 $answer_obj = $this->answers[$key];
423 $next_id = $this->db->nextId('qpl_a_textsubset');
424 $this->db->manipulateF(
425 "INSERT INTO qpl_a_textsubset (answer_id, question_fi, answertext, points, aorder, tstamp) VALUES (%s, %s, %s, %s, %s, %s)",
426 [ 'integer', 'integer', 'text', 'float', 'integer', 'integer' ],
427 [
428 $next_id,
429 $this->getId(),
430 $answer_obj->getAnswertext(),
431 $answer_obj->getPoints(),
432 $answer_obj->getOrder(),
433 time()
434 ]
435 );
436 }
437 }
438
445 public function getQuestionType(): string
446 {
447 return "assTextSubset";
448 }
449
454 public function &joinAnswers(): array
455 {
456 $join = [];
457 foreach ($this->answers as $answer) {
458 $key = $answer->getPoints() . '';
459
460 if (!isset($join[$key]) || !is_array($join[$key])) {
461 $join[$key] = [];
462 }
463
464 $join[$key][] = $answer->getAnswertext();
465 }
466
467 return $join;
468 }
469
476 public function getMaxTextboxWidth(): int
477 {
478 $maxwidth = 0;
479 foreach ($this->answers as $answer) {
480 $len = strlen($answer->getAnswertext());
481 if ($len > $maxwidth) {
482 $maxwidth = $len;
483 }
484 }
485 return $maxwidth + 3;
486 }
487
494 public function getAdditionalTableName(): string
495 {
496 return "qpl_qst_textsubset";
497 }
498
505 public function getAnswerTableName(): string
506 {
507 return "qpl_a_textsubset";
508 }
509
514 public function getRTETextWithMediaObjects(): string
515 {
516 return parent::getRTETextWithMediaObjects();
517 }
518
519 public function getAnswers(): array
520 {
521 return $this->answers;
522 }
523
527 public function toJSON(): string
528 {
529 $result = [];
530 $result['id'] = $this->getId();
531 $result['type'] = (string) $this->getQuestionType();
532 $result['title'] = $this->getTitleForHTMLOutput();
533 $result['question'] = $this->formatSAQuestion($this->getQuestion());
534 $result['nr_of_tries'] = $this->getNrOfTries();
535 $result['matching_method'] = $this->getTextRating();
536 $result['feedback'] = [
537 'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
538 'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
539 ];
540
541 $answers = [];
542 foreach ($this->getAnswers() as $key => $answer_obj) {
543 $answers[] = [
544 "answertext" => (string) $answer_obj->getAnswertext(),
545 "points" => (float) $answer_obj->getPoints(),
546 "order" => (int) $answer_obj->getOrder()
547 ];
548 }
549 $result['correct_answers'] = $answers;
550
551 $answers = [];
552 for ($loop = 1; $loop <= $this->getCorrectAnswers(); $loop++) {
553 $answers[] = [
554 "answernr" => $loop
555 ];
556 }
557 $result['answers'] = $answers;
558
559 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
560 $result['mobs'] = $mobs;
561
562 return json_encode($result);
563 }
564
568 protected function getSolutionSubmit(): array
569 {
570 $purifier = $this->getHtmlUserSolutionPurifier();
571 $post = $this->dic->http()->wrapper()->post();
572
573 $solutionSubmit = [];
574 foreach ($this->getAnswers() as $index => $a) {
575 if ($post->has("TEXTSUBSET_$index")) {
576 $value = $post->retrieve(
577 "TEXTSUBSET_$index",
578 $this->dic->refinery()->kindlyTo()->string()
579 );
580 if ($value) {
581 $value = $this->extendedTrim($value);
582 $value = $purifier->purify($value);
583 $solutionSubmit[] = $value;
584 }
585 }
586 }
587
588 return $solutionSubmit;
589 }
590
591 protected function calculateReachedPointsForSolution(?array $enteredTexts): float
592 {
593 $enteredTexts ??= [];
594 $available_answers = $this->getAvailableAnswers();
595 $points = 0.0;
596 foreach ($enteredTexts as $enteredtext) {
597 $index = $this->isAnswerCorrect($available_answers, html_entity_decode($enteredtext));
598 if ($index !== false) {
599 unset($available_answers[$index]);
600 $points += $this->answers[$index]->getPoints();
601 }
602 }
603 return $points;
604 }
605
606 public function getOperators(string $expression): array
607 {
609 }
610
611 public function getExpressionTypes(): array
612 {
613 return [
618 ];
619 }
620
621 public function getUserQuestionResult(
622 int $active_id,
623 int $pass
625 $result = new ilUserQuestionResult($this, $active_id, $pass);
626
627 $maxStep = $this->lookupMaxStep($active_id, $pass);
628 if ($maxStep > 0) {
629 $data = $this->db->queryF(
630 "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s ORDER BY solution_id",
631 ["integer", "integer", "integer","integer"],
632 [$active_id, $pass, $this->getId(), $maxStep]
633 );
634 } else {
635 $data = $this->db->queryF(
636 "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s ORDER BY solution_id",
637 ["integer", "integer", "integer"],
638 [$active_id, $pass, $this->getId()]
639 );
640 }
641
642 for ($index = 1; $index <= $this->db->numRows($data); ++$index) {
643 $row = $this->db->fetchAssoc($data);
644 $result->addKeyValue($index, $row["value1"]);
645 }
646
647 $points = $this->calculateReachedPoints($active_id, $pass);
648 $max_points = $this->getMaximumPoints();
649
650 $result->setReachedPercentage(($points / $max_points) * 100);
651
652 return $result;
653 }
654
661 public function getAvailableAnswerOptions($index = null)
662 {
663 if ($index !== null) {
664 return $this->getAnswer($index);
665 } else {
666 return $this->getAnswers();
667 }
668 }
669
670 public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
671 {
672 $found = false;
673
674 foreach ($this->getAnswers() as $item) {
675 if ($answerOptionValue !== $item->getAnswerText()) {
676 continue;
677 }
678
679 $found = true;
680 break;
681 }
682
683 return !$found;
684 }
685
686 public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
687 {
688 $this->addAnswer($answerOptionValue, $points, $qIndex);
689 }
690
691 public function toLog(AdditionalInformationGenerator $additional_info): array
692 {
693 return [
694 AdditionalInformationGenerator::KEY_QUESTION_TYPE => (string) $this->getQuestionType(),
695 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $this->getTitleForHTMLOutput(),
696 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $this->formatSAQuestion($this->getQuestion()),
697 AdditionalInformationGenerator::KEY_QUESTION_TEXT_MATCHING_METHOD => $additional_info->getTagForLangVar(
698 $this->getMatchingMethodLangVar($this->getTextRating())
699 ),
700 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTIONS => array_map(
701 fn(ASS_AnswerBinaryStateImage $answer) => [
702 AdditionalInformationGenerator::KEY_QUESTION_ANSWER_OPTION => $answer->getAnswertext(),
703 AdditionalInformationGenerator::KEY_QUESTION_REACHABLE_POINTS => $answer->getPoints()
704 ],
705 $this->getAnswers()
706 ),
707 AdditionalInformationGenerator::KEY_FEEDBACK => [
708 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_INCOMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
709 AdditionalInformationGenerator::KEY_QUESTION_FEEDBACK_ON_COMPLETE => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
710 ]
711 ];
712 }
713
714 private function getMatchingMethodLangVar(string $matching_method): string
715 {
716 switch ($matching_method) {
718 return 'cloze_textgap_case_insensitive';
720 return 'cloze_textgap_case_sensitive';
722 return 'cloze_textgap_levenshtein_of:1';
724 return 'cloze_textgap_levenshtein_of:2';
726 return 'cloze_textgap_levenshtein_of:3';
728 return 'cloze_textgap_levenshtein_of:4';
730 return 'cloze_textgap_levenshtein_of:5';
731 default:
732 return '';
733 }
734 }
735
736 protected function solutionValuesToLog(
737 AdditionalInformationGenerator $additional_info,
738 array $solution_values
739 ): array {
740 return array_map(
741 static fn(array $v): string => $v['value1'],
742 $solution_values
743 );
744 }
745
746 public function solutionValuesToText(array $solution_values): array
747 {
748 return array_map(
749 static fn(array $v): string => $v['value1'],
750 $solution_values
751 );
752 }
753
754 public function getCorrectSolutionForTextOutput(int $active_id, int $pass): array
755 {
756 return $this->getAvailableAnswers();
757 }
758}
Class for answers with a binary state indicator.
getPoints()
Gets the points.
getAnswertext()
Gets the answer text.
const TEXTGAP_RATING_CASESENSITIVE
const TEXTGAP_RATING_LEVENSHTEIN1
const TEXTGAP_RATING_LEVENSHTEIN5
const TEXTGAP_RATING_CASEINSENSITIVE
const TEXTGAP_RATING_LEVENSHTEIN4
const TEXTGAP_RATING_LEVENSHTEIN2
const TEXTGAP_RATING_LEVENSHTEIN3
setOriginalId(?int $original_id)
setId(int $id=-1)
Refinery $refinery
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
setQuestion(string $question="")
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
setAuthor(string $author="")
setComment(string $comment="")
setObjId(int $obj_id=0)
getSolutionMaxPass(int $active_id)
setOwner(int $owner=-1)
setNrOfTries(int $a_nr_of_tries)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
setTitle(string $title="")
saveQuestionDataToDb(?int $original_id=null)
setPoints(float $points)
Class for TextSubset questions.
getExpressionTypes()
Get all available expression types for a specific question.
getMaxTextboxWidth()
Returns the maximum width needed for the answer textboxes.
setCorrectAnswers(int $a_correct_answers)
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
getAdditionalTableName()
Returns the name of the additional question data table in the database.
isAnswerCorrect($answers, $answer)
Returns the index of the found answer, if the given answer is in the set of correct answers and match...
loadFromDb(int $question_id)
getCorrectSolutionForTextOutput(int $active_id, int $pass)
& joinAnswers()
Returns the answers of the question as a comma separated string.
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
toJSON()
Returns a JSON representation of the question.
deleteAnswer($index=0)
Deletes an answer with a given index.
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.
saveToDb(?int $original_id=null)
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
getOperators(string $expression)
Get all available operations for a specific question.
calculateReachedPointsForSolution(?array $enteredTexts)
setTextRating(string $text_rating)
addAnswer($answertext, $points, $order)
Adds an answer to the question.
getAnswerTableName()
Returns the name of the answer table in the database.
getUserQuestionResult(int $active_id, int $pass)
Get the user solution for a question by active_id and the test pass.
getAvailableAnswers()
Returns the available answers for the question.
getMatchingMethodLangVar(string $matching_method)
getQuestionType()
Returns the question type of the question.
getAnswerCount()
Returns the number of answers.
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
flushAnswers()
Deletes all answers.
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
getAnswer($index=0)
Returns an answer with a given index.
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...
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...
return['delivery_method'=> 'php',]
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...
$post
Definition: ltitoken.php:46
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$counter