ILIAS  release_7 Revision v7.30-3-g800a261c036
All Data Structures Namespaces Files Functions Variables Modules Pages
class.assClozeTest.php
Go to the documentation of this file.
1 <?php
2 /* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3 
4 require_once './Modules/TestQuestionPool/classes/class.assQuestion.php';
5 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
6 require_once './Modules/TestQuestionPool/classes/class.assClozeGapCombination.php';
7 require_once './Modules/TestQuestionPool/interfaces/interface.ilObjQuestionScoringAdjustable.php';
8 require_once './Modules/TestQuestionPool/interfaces/interface.ilObjAnswerScoringAdjustable.php';
9 require_once './Modules/TestQuestionPool/interfaces/interface.iQuestionCondition.php';
10 require_once './Modules/TestQuestionPool/classes/class.ilUserQuestionResult.php';
11 require_once 'Modules/TestQuestionPool/classes/feedback/class.ilAssClozeTestFeedback.php';
12 
25 {
33  public $gaps;
34 
43 
44 
46 
54  public $start_tag;
55 
63  public $end_tag;
64 
76 
87 
94 
95  public $cloze_text;
96 
100  public $feedbackOBJ;
101 
103 
117  public function __construct(
118  $title = "",
119  $comment = "",
120  $author = "",
121  $owner = -1,
122  $question = ""
123  ) {
125  $this->start_tag = "[gap]";
126  $this->end_tag = "[/gap]";
127  $this->gaps = array();
128  $this->setQuestion($question); // @TODO: Should this be $question?? See setter for why this is not trivial.
129  $this->fixedTextLength = "";
130  $this->identical_scoring = 1;
131  $this->gap_combinations_exists = false;
132  $this->gap_combinations = array();
133  }
134 
140  public function isComplete()
141  {
142  if (strlen($this->getTitle())
143  && $this->getAuthor()
144  && $this->getClozeText()
145  && count($this->getGaps())
146  && $this->getMaximumPoints() > 0) {
147  return true;
148  }
149  return false;
150  }
151 
159  public function cleanQuestiontext($text)
160  {
161  // fau: fixGapReplace - mask dollars for replacement
162  $text = str_replace('$', 'GAPMASKEDDOLLAR', $text);
163  $text = preg_replace("/\[gap[^\]]*?\]/", "[gap]", $text);
164  $text = preg_replace("/<gap([^>]*?)>/", "[gap]", $text);
165  $text = str_replace("</gap>", "[/gap]", $text);
166  $text = str_replace('GAPMASKEDDOLLAR', '$', $text);
167  // fau.
168  return $text;
169  }
170 
171  // fau: fixGapReplace - add function replaceFirstGap()
178  public function replaceFirstGap($gaptext, $content)
179  {
180  $content = str_replace('$', 'GAPMASKEDDOLLAR', $content);
181  $output = preg_replace("/\[gap\].*?\[\/gap\]/", $content, $gaptext, 1);
182  $output = str_replace('GAPMASKEDDOLLAR', '$', $output);
183 
184  return $output;
185  }
186  // fau.
193  public function loadFromDb($question_id)
194  {
195  global $DIC;
196  $ilDB = $DIC['ilDB'];
197  $result = $ilDB->queryF(
198  "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",
199  array("integer"),
200  array($question_id)
201  );
202  if ($result->numRows() == 1) {
203  $data = $ilDB->fetchAssoc($result);
204  $this->setId($question_id);
205  $this->setNrOfTries($data['nr_of_tries']);
206  $this->setObjId($data["obj_fi"]);
207  $this->setTitle($data["title"]);
208  $this->setComment($data["description"]);
209  $this->setOriginalId($data["original_id"]);
210  $this->setAuthor($data["author"]);
211  $this->setPoints($data["points"]);
212  $this->setOwner($data["owner"]);
213  $this->setQuestion($this->cleanQuestiontext($data["question_text"]));
214  $this->setClozeText($data['cloze_text']);
215  $this->setFixedTextLength($data["fixed_textlen"]);
216  $this->setIdenticalScoring(($data['tstamp'] == 0) ? true : $data["identical_scoring"]);
217  $this->setFeedbackMode($data['feedback_mode'] === null ? ilAssClozeTestFeedback::FB_MODE_GAP_QUESTION : $data['feedback_mode']);
218 
219  try {
220  $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
223  }
224 
225  // replacement of old syntax with new syntax
226  include_once("./Services/RTE/classes/class.ilRTE.php");
227  $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1);
228  $this->cloze_text = ilRTE::_replaceMediaObjectImageSrc($this->cloze_text, 1);
229  $this->setTextgapRating($data["textgap_rating"]);
230  $this->setEstimatedWorkingTime(substr($data["working_time"], 0, 2), substr($data["working_time"], 3, 2), substr($data["working_time"], 6, 2));
231 
232  try {
233  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
234  } catch (ilTestQuestionPoolException $e) {
235  }
236 
237  // open the cloze gaps with all answers
238  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
239  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
240  $result = $ilDB->queryF(
241  "SELECT * FROM qpl_a_cloze WHERE question_fi = %s ORDER BY gap_id, aorder ASC",
242  array("integer"),
243  array($question_id)
244  );
245  if ($result->numRows() > 0) {
246  $this->gaps = array();
247  while ($data = $ilDB->fetchAssoc($result)) {
248  switch ($data["cloze_type"]) {
249  case CLOZE_TEXT:
250  if (!array_key_exists($data["gap_id"], $this->gaps)) {
251  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_TEXT);
252  }
253  $answer = new assAnswerCloze(
254  $data["answertext"],
255  $data["points"],
256  $data["aorder"]
257  );
258  $this->gaps[$data["gap_id"]]->setGapSize($data['gap_size']);
259 
260  $this->gaps[$data["gap_id"]]->addItem($answer);
261  break;
262  case CLOZE_SELECT:
263  if (!array_key_exists($data["gap_id"], $this->gaps)) {
264  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_SELECT);
265  $this->gaps[$data["gap_id"]]->setShuffle($data["shuffle"]);
266  }
267  $answer = new assAnswerCloze(
268  $data["answertext"],
269  $data["points"],
270  $data["aorder"]
271  );
272  $this->gaps[$data["gap_id"]]->addItem($answer);
273  break;
274  case CLOZE_NUMERIC:
275  if (!array_key_exists($data["gap_id"], $this->gaps)) {
276  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_NUMERIC);
277  }
278  $answer = new assAnswerCloze(
279  $data["answertext"],
280  $data["points"],
281  $data["aorder"]
282  );
283  $this->gaps[$data["gap_id"]]->setGapSize($data['gap_size']);
284  $answer->setLowerBound($data["lowerlimit"]);
285  $answer->setUpperBound($data["upperlimit"]);
286  $this->gaps[$data["gap_id"]]->addItem($answer);
287  break;
288  }
289  }
290  }
291  }
292  $assClozeGapCombinationObj = new assClozeGapCombination();
293  $check_for_gap_combinations = $assClozeGapCombinationObj->loadFromDb($question_id);
294  if (count($check_for_gap_combinations) != 0) {
295  $this->setGapCombinationsExists(true);
296  $this->setGapCombinations($check_for_gap_combinations);
297  }
298  parent::loadFromDb($question_id);
299  }
300 
301  #region Save question to db
302 
312  public function saveToDb($original_id = "")
313  {
317 
318  parent::saveToDb($original_id);
319  }
320 
324  public function saveAnswerSpecificDataToDb()
325  {
326  global $DIC;
327  $ilDB = $DIC['ilDB'];
328 
329  $ilDB->manipulateF(
330  "DELETE FROM qpl_a_cloze WHERE question_fi = %s",
331  array( "integer" ),
332  array( $this->getId() )
333  );
334 
335  foreach ($this->gaps as $key => $gap) {
336  $this->saveClozeGapItemsToDb($gap, $key);
337  }
338  }
339 
346  {
347  global $DIC; /* @var ILIAS\DI\Container $DIC */
348 
349 
350  $DIC->database()->manipulateF(
351  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
352  array( "integer" ),
353  array( $this->getId() )
354  );
355 
356  $DIC->database()->insert($this->getAdditionalTableName(), array(
357  'question_fi' => array('integer', $this->getId()),
358  'textgap_rating' => array('text', $this->getTextgapRating()),
359  'identical_scoring' => array('text', $this->getIdenticalScoring()),
360  'fixed_textlen' => array('integer', $this->getFixedTextLength() ? $this->getFixedTextLength() : null),
361  'cloze_text' => array('text', ilRTE::_replaceMediaObjectImageSrc($this->getClozeText(), 0)),
362  'feedback_mode' => array('text', $this->getFeedbackMode())
363  ));
364  }
365 
372  protected function saveClozeGapItemsToDb($gap, $key)
373  {
374  global $DIC;
375  $ilDB = $DIC['ilDB'];
376  foreach ($gap->getItems($this->getShuffler()) as $item) {
377  $query = "";
378  $next_id = $ilDB->nextId('qpl_a_cloze');
379  switch ($gap->getType()) {
380  case CLOZE_TEXT:
381  $this->saveClozeTextGapRecordToDb($next_id, $key, $item, $gap);
382  break;
383  case CLOZE_SELECT:
384  $this->saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap);
385  break;
386  case CLOZE_NUMERIC:
387  $this->saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap);
388  break;
389  }
390  }
391  }
392 
401  protected function saveClozeTextGapRecordToDb($next_id, $key, $item, $gap)
402  {
403  global $DIC;
404  $ilDB = $DIC['ilDB'];
405  $ilDB->manipulateF(
406  "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)",
407  array(
408  "integer",
409  "integer",
410  "integer",
411  "text",
412  "float",
413  "integer",
414  "text",
415  "integer"
416  ),
417  array(
418  $next_id,
419  $this->getId(),
420  $key,
421  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
422  $item->getPoints(),
423  $item->getOrder(),
424  $gap->getType(),
425  (int) $gap->getGapSize()
426  )
427  );
428  }
429 
438  protected function saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap)
439  {
440  global $DIC;
441  $ilDB = $DIC['ilDB'];
442  $ilDB->manipulateF(
443  "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)",
444  array(
445  "integer",
446  "integer",
447  "integer",
448  "text",
449  "float",
450  "integer",
451  "text",
452  "text"
453  ),
454  array(
455  $next_id,
456  $this->getId(),
457  $key,
458  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
459  $item->getPoints(),
460  $item->getOrder(),
461  $gap->getType(),
462  ($gap->getShuffle()) ? "1" : "0"
463  )
464  );
465  }
466 
475  protected function saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap)
476  {
477  global $DIC;
478  $ilDB = $DIC['ilDB'];
479 
480  include_once "./Services/Math/classes/class.EvalMath.php";
481  $eval = new EvalMath();
482  $eval->suppress_errors = true;
483  $ilDB->manipulateF(
484  "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)",
485  array(
486  "integer",
487  "integer",
488  "integer",
489  "text",
490  "float",
491  "integer",
492  "text",
493  "text",
494  "text",
495  "integer"
496  ),
497  array(
498  $next_id,
499  $this->getId(),
500  $key,
501  strlen($item->getAnswertext()) ? $item->getAnswertext() : "",
502  $item->getPoints(),
503  $item->getOrder(),
504  $gap->getType(),
505  ($eval->e($item->getLowerBound() !== false) && strlen(
506  $item->getLowerBound()
507  ) > 0) ? $item->getLowerBound() : $item->getAnswertext(),
508  ($eval->e($item->getUpperBound() !== false) && strlen(
509  $item->getUpperBound()
510  ) > 0) ? $item->getUpperBound() : $item->getAnswertext(),
511  (int) $gap->getGapSize()
512  )
513  );
514  }
515 
516 
517 
518  #endregion Save question to db
519 
526  public function getGaps()
527  {
528  return $this->gaps;
529  }
530 
531 
538  public function flushGaps()
539  {
540  $this->gaps = array();
541  }
542 
552  public function setClozeText($cloze_text = "")
553  {
554  $this->gaps = [];
555  $this->cloze_text = $this->cleanQuestiontext($cloze_text);
557  }
558 
559  public function setClozeTextValue($cloze_text = "")
560  {
561  $this->cloze_text = $cloze_text;
562  }
563 
571  public function getClozeText()
572  {
573  return $this->cloze_text;
574  }
575 
584  public function getClozeTextForHTMLOutput() : string
585  {
586  $gaps = [];
587  preg_match_all('/\[gap\].*?\[\/gap\]/', $this->getClozeText(), $gaps);
588  $string_with_replaced_gaps = str_replace($gaps[0], '######GAP######', $this->getClozeText());
589  $cleaned_text = $this->getHtmlQuestionContentPurifier()->purify(
590  $string_with_replaced_gaps
591  );
592  $cleaned_text_with_gaps = preg_replace_callback('/######GAP######/', function ($match) use (&$gaps) {
593  return array_shift($gaps[0]);
594  }, $cleaned_text);
595 
597  || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
598  $cleaned_text_with_gaps = nl2br($cleaned_text_with_gaps);
599  }
600 
601  return $this->prepareTextareaOutput($cleaned_text_with_gaps, true);
602  }
603 
611  public function getStartTag()
612  {
613  return $this->start_tag;
614  }
615 
623  public function setStartTag($start_tag = "[gap]")
624  {
625  $this->start_tag = $start_tag;
626  }
627 
635  public function getEndTag()
636  {
637  return $this->end_tag;
638  }
639 
647  public function setEndTag($end_tag = "[/gap]")
648  {
649  $this->end_tag = $end_tag;
650  }
651 
655  public function getFeedbackMode()
656  {
657  return $this->feedbackMode;
658  }
659 
664  {
665  $this->feedbackMode = $feedbackMode;
666  }
667 
674  public function createGapsFromQuestiontext()
675  {
676  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
677  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
678  $search_pattern = "|\[gap\](.*?)\[/gap\]|i";
679  preg_match_all($search_pattern, $this->getClozeText(), $found);
680  $this->gaps = array();
681  if (count($found[0])) {
682  foreach ($found[1] as $gap_index => $answers) {
683  // create text gaps by default
684  $gap = new assClozeGap(CLOZE_TEXT);
685  $textparams = preg_split("/(?<!\\\\),/", $answers);
686  foreach ($textparams as $key => $value) {
687  $answer = new assAnswerCloze($value, 0, $key);
688  $gap->addItem($answer);
689  }
690  $this->gaps[$gap_index] = $gap;
691  }
692  }
693  }
694 
700  public function setGapType($gap_index, $gap_type)
701  {
702  if (array_key_exists($gap_index, $this->gaps)) {
703  $this->gaps[$gap_index]->setType($gap_type);
704  }
705  }
706 
716  public function setGapShuffle($gap_index = 0, $shuffle = 1)
717  {
718  if (array_key_exists($gap_index, $this->gaps)) {
719  $this->gaps[$gap_index]->setShuffle($shuffle);
720  }
721  }
722 
729  public function clearGapAnswers()
730  {
731  foreach ($this->gaps as $gap_index => $gap) {
732  $this->gaps[$gap_index]->clearItems();
733  }
734  }
735 
743  public function getGapCount()
744  {
745  if (is_array($this->gaps)) {
746  return count($this->gaps);
747  } else {
748  return 0;
749  }
750  }
751 
762  public function addGapAnswer($gap_index, $order, $answer)
763  {
764  if (array_key_exists($gap_index, $this->gaps)) {
765  if ($this->gaps[$gap_index]->getType() == CLOZE_NUMERIC) {
766  // only allow notation with "." for real numbers
767  $answer = str_replace(",", ".", $answer);
768  }
769  $this->gaps[$gap_index]->addItem(new assAnswerCloze(trim($answer), 0, $order));
770  }
771  }
772 
781  public function getGap($gap_index = 0)
782  {
783  if (array_key_exists($gap_index, $this->gaps)) {
784  return $this->gaps[$gap_index];
785  } else {
786  return null;
787  }
788  }
789 
790  public function setGapSize($gap_index, $order, $size)
791  {
792  if (array_key_exists($gap_index, $this->gaps)) {
793  $this->gaps[$gap_index]->setGapSize($size);
794  }
795  }
796 
807  public function setGapAnswerPoints($gap_index, $order, $points)
808  {
809  if (array_key_exists($gap_index, $this->gaps)) {
810  $this->gaps[$gap_index]->setItemPoints($order, $points);
811  }
812  }
813 
822  public function addGapText($gap_index)
823  {
824  if (array_key_exists($gap_index, $this->gaps)) {
825  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
826  $answer = new assAnswerCloze(
827  "",
828  0,
829  $this->gaps[$gap_index]->getItemCount()
830  );
831  $this->gaps[$gap_index]->addItem($answer);
832  }
833  }
834 
843  public function addGapAtIndex($gap, $index)
844  {
845  $this->gaps[$index] = $gap;
846  }
847 
858  public function setGapAnswerLowerBound($gap_index, $order, $bound)
859  {
860  if (array_key_exists($gap_index, $this->gaps)) {
861  $this->gaps[$gap_index]->setItemLowerBound($order, $bound);
862  }
863  }
864 
875  public function setGapAnswerUpperBound($gap_index, $order, $bound)
876  {
877  if (array_key_exists($gap_index, $this->gaps)) {
878  $this->gaps[$gap_index]->setItemUpperBound($order, $bound);
879  }
880  }
881 
888  public function getMaximumPoints()
889  {
890  $assClozeGapCombinationObj = new assClozeGapCombination();
891  $points = 0;
892  $gaps_used_in_combination = array();
893  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
894  $points = $assClozeGapCombinationObj->getMaxPointsForCombination($this->getId());
895  $gaps_used_in_combination = $assClozeGapCombinationObj->getGapsWhichAreUsedInCombination($this->getId());
896  }
897  foreach ($this->gaps as $gap_index => $gap) {
898  if (!array_key_exists($gap_index, $gaps_used_in_combination)) {
899  if ($gap->getType() == CLOZE_TEXT) {
900  $gap_max_points = 0;
901  foreach ($gap->getItems($this->getShuffler()) as $item) {
902  if ($item->getPoints() > $gap_max_points) {
903  $gap_max_points = $item->getPoints();
904  }
905  }
906  $points += $gap_max_points;
907  } elseif ($gap->getType() == CLOZE_SELECT) {
908  $srpoints = 0;
909  foreach ($gap->getItems($this->getShuffler()) as $item) {
910  if ($item->getPoints() > $srpoints) {
911  $srpoints = $item->getPoints();
912  }
913  }
914  $points += $srpoints;
915  } elseif ($gap->getType() == CLOZE_NUMERIC) {
916  $numpoints = 0;
917  foreach ($gap->getItems($this->getShuffler()) as $item) {
918  if ($item->getPoints() > $numpoints) {
919  $numpoints = $item->getPoints();
920  }
921  }
922  $points += $numpoints;
923  }
924  }
925  }
926 
927  return $points;
928  }
929 
935  public function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null)
936  {
937  if ($this->id <= 0) {
938  // The question has not been saved. It cannot be duplicated
939  return;
940  }
941  // duplicate the question in database
942  $this_id = $this->getId();
943  $thisObjId = $this->getObjId();
944 
945  $clone = $this;
946  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
948  $clone->id = -1;
949 
950  if ((int) $testObjId > 0) {
951  $clone->setObjId($testObjId);
952  }
953 
954  if ($title) {
955  $clone->setTitle($title);
956  }
957  if ($author) {
958  $clone->setAuthor($author);
959  }
960  if ($owner) {
961  $clone->setOwner($owner);
962  }
963  if ($for_test) {
964  $clone->saveToDb($original_id);
965  } else {
966  $clone->saveToDb();
967  }
968  if ($this->gap_combinations_exists) {
969  $this->copyGapCombination($this_id, $clone->getId());
970  }
971  if ($for_test) {
972  $clone->saveToDb($original_id);
973  } else {
974  $clone->saveToDb();
975  }
976  // copy question page content
977  $clone->copyPageOfQuestion($this_id);
978  // copy XHTML media objects
979  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
980 
981  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
982 
983  return $clone->getId();
984  }
985 
991  public function copyObject($target_questionpool_id, $title = "")
992  {
993  if ($this->getId() <= 0) {
994  // The question has not been saved. It cannot be duplicated
995  return;
996  }
997 
998  $thisId = $this->getId();
999  $thisObjId = $this->getObjId();
1000 
1001  $clone = $this;
1002  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
1004  $clone->id = -1;
1005  $clone->setObjId($target_questionpool_id);
1006  if ($title) {
1007  $clone->setTitle($title);
1008  }
1009 
1010  $clone->saveToDb();
1011 
1012  if ($this->gap_combinations_exists) {
1013  $this->copyGapCombination($original_id, $clone->getId());
1014  $clone->saveToDb();
1015  }
1016 
1017  // copy question page content
1018  $clone->copyPageOfQuestion($original_id);
1019  // copy XHTML media objects
1020  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
1021 
1022  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
1023 
1024  return $clone->getId();
1025  }
1026 
1027  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "")
1028  {
1029  if ($this->id <= 0) {
1030  // The question has not been saved. It cannot be duplicated
1031  return;
1032  }
1033 
1034  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
1035 
1036  $sourceQuestionId = $this->id;
1037  $sourceParentId = $this->getObjId();
1038 
1039  // duplicate the question in database
1040  $clone = $this;
1041  $clone->id = -1;
1042 
1043  $clone->setObjId($targetParentId);
1044 
1045  if ($targetQuestionTitle) {
1046  $clone->setTitle($targetQuestionTitle);
1047  }
1048 
1049  $clone->saveToDb();
1050 
1051  if ($this->gap_combinations_exists) {
1052  $this->copyGapCombination($sourceQuestionId, $clone->getId());
1053  $clone->saveToDb();
1054  }
1055  // copy question page content
1056  $clone->copyPageOfQuestion($sourceQuestionId);
1057  // copy XHTML media objects
1058  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
1059 
1060  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
1061 
1062  return $clone->id;
1063  }
1064 
1065  public function copyGapCombination($orgID, $newID)
1066  {
1067  $assClozeGapCombinationObj = new assClozeGapCombination();
1068  $array = $assClozeGapCombinationObj->loadFromDb($orgID);
1069  $assClozeGapCombinationObj->importGapCombinationToDb($newID, $array);
1070  }
1071 
1077  public function updateClozeTextFromGaps()
1078  {
1079  $output = $this->getClozeText();
1080  foreach ($this->getGaps() as $gap_index => $gap) {
1081  $answers = array();
1082  foreach ($gap->getItemsRaw() as $item) {
1083  array_push($answers, str_replace([',', '['], ["\\,", '[&hairsp;'], $item->getAnswerText()));
1084  }
1085  // fau: fixGapReplace - use replace function
1086  $output = $this->replaceFirstGap($output, "[_gap]" . $this->prepareTextareaOutput(join(",", $answers), true) . "[/_gap]");
1087  // fau.
1088  }
1089  $output = str_replace("_gap]", "gap]", $output);
1090  $this->cloze_text = $output;
1091  }
1092 
1102  public function deleteAnswerText($gap_index, $answer_index)
1103  {
1104  if (array_key_exists($gap_index, $this->gaps)) {
1105  if ($this->gaps[$gap_index]->getItemCount() == 1) {
1106  // this is the last answer text => remove the gap
1107  $this->deleteGap($gap_index);
1108  } else {
1109  // remove the answer text
1110  $this->gaps[$gap_index]->deleteItem($answer_index);
1111  $this->updateClozeTextFromGaps();
1112  }
1113  }
1114  }
1115 
1124  public function deleteGap($gap_index)
1125  {
1126  if (array_key_exists($gap_index, $this->gaps)) {
1127  $output = $this->getClozeText();
1128  foreach ($this->getGaps() as $replace_gap_index => $gap) {
1129  $answers = array();
1130  foreach ($gap->getItemsRaw() as $item) {
1131  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
1132  }
1133  if ($replace_gap_index == $gap_index) {
1134  // fau: fixGapReplace - use replace function
1135  $output = $this->replaceFirstGap($output, '');
1136  // fau.
1137  } else {
1138  // fau: fixGapReplace - use replace function
1139  $output = $this->replaceFirstGap($output, "[_gap]" . join(",", $answers) . "[/_gap]");
1140  // fau.
1141  }
1142  }
1143  $output = str_replace("_gap]", "gap]", $output);
1144  $this->cloze_text = $output;
1145  unset($this->gaps[$gap_index]);
1146  $this->gaps = array_values($this->gaps);
1147  }
1148  }
1149 
1159  public function getTextgapPoints($a_original, $a_entered, $max_points)
1160  {
1161  include_once "./Services/Utilities/classes/class.ilStr.php";
1162  global $DIC;
1163  $refinery = $DIC->refinery();
1164  $result = 0;
1165  $gaprating = $this->getTextgapRating();
1166 
1167  switch ($gaprating) {
1169  if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) {
1170  $result = $max_points;
1171  }
1172  break;
1174  if (strcmp($a_original, $a_entered) == 0) {
1175  $result = $max_points;
1176  }
1177  break;
1179  $transformation = $refinery->string()->levenshtein()->standard($a_original, 1);
1180  break;
1182  $transformation = $refinery->string()->levenshtein()->standard($a_original, 2);
1183  break;
1185  $transformation = $refinery->string()->levenshtein()->standard($a_original, 3);
1186  break;
1188  $transformation = $refinery->string()->levenshtein()->standard($a_original, 4);
1189  break;
1191  $transformation = $refinery->string()->levenshtein()->standard($a_original, 5);
1192  break;
1193  }
1194 
1195  // run answers against Levenshtein2 methods
1196  if (isset($transformation) && $transformation->transform($a_entered) >= 0) {
1197  $result = $max_points;
1198  }
1199  return $result;
1200  }
1201 
1202 
1212  public function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound)
1213  {
1214  // fau: fixGapFormula - check entered value by evalMath
1215  // if( ! $this->checkForValidFormula($a_entered) )
1216  // {
1217  // return 0;
1218  // }
1219 
1220  include_once "./Services/Math/classes/class.EvalMath.php";
1221  $eval = new EvalMath();
1222  $eval->suppress_errors = true;
1223  $result = 0;
1224 
1225  if ($eval->e($a_entered) === false) {
1226  return 0;
1227  } elseif (($eval->e($lowerBound) !== false) && ($eval->e($upperBound) !== false)) {
1228  // fau.
1229  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
1230  $result = $max_points;
1231  }
1232  } elseif ($eval->e($lowerBound) !== false) {
1233  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) {
1234  $result = $max_points;
1235  }
1236  } elseif ($eval->e($upperBound) !== false) {
1237  if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) {
1238  $result = $max_points;
1239  }
1240  } else {
1241  if ($eval->e($a_entered) == $eval->e($a_original)) {
1242  $result = $max_points;
1243  }
1244  }
1245  return $result;
1246  }
1247 
1252  public function checkForValidFormula($value)
1253  {
1254  return preg_match("/^-?(\\d*)(,|\\.|\\/){0,1}(\\d*)$/", $value, $matches);
1255  }
1266  public function calculateReachedPoints($active_id, $pass = null, $authorized = true, $returndetails = false)
1267  {
1268  global $DIC;
1269  $ilDB = $DIC['ilDB'];
1270 
1271  if (is_null($pass)) {
1272  $pass = $this->getSolutionMaxPass($active_id);
1273  }
1274 
1275  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorized);
1276  $user_result = array();
1277  while ($data = $ilDB->fetchAssoc($result)) {
1278  if (strcmp($data["value2"], "") != 0) {
1279  $user_result[$data["value1"]] = array(
1280  "gap_id" => $data["value1"],
1281  "value" => $data["value2"]
1282  );
1283  }
1284  }
1285 
1286  ksort($user_result); // this is required when identical scoring for same solutions is disabled
1287 
1288  if ($returndetails) {
1289  $detailed = array();
1290  $this->calculateReachedPointsForSolution($user_result, $detailed);
1291  return $detailed;
1292  }
1293 
1294  return $this->calculateReachedPointsForSolution($user_result);
1295  }
1296 
1297  protected function isValidNumericSubmitValue($submittedValue)
1298  {
1299  if (is_numeric($submittedValue)) {
1300  return true;
1301  }
1302 
1303  if (preg_match('/^[-+]{0,1}\d+\/\d+$/', $submittedValue)) {
1304  return true;
1305  }
1306 
1307  return false;
1308  }
1309 
1310  public function validateSolutionSubmit()
1311  {
1312  foreach ($this->getSolutionSubmitValidation() as $gapIndex => $value) {
1313  $gap = $this->getGap($gapIndex);
1314 
1315  if ($gap->getType() != CLOZE_NUMERIC) {
1316  continue;
1317  }
1318 
1319  if (strlen($value) && !$this->isValidNumericSubmitValue($value)) {
1320  ilUtil::sendFailure($this->lng->txt("err_no_numeric_value"), true);
1321  return false;
1322  }
1323  }
1324 
1325  return true;
1326  }
1327 
1328  public function fetchSolutionSubmit($submit)
1329  {
1330  $solutionSubmit = array();
1331  foreach ($submit as $key => $value) {
1332  if ($value === null || is_array($value)) {
1333  continue;
1334  }
1335 
1336  $trimmed_value = trim($value);
1337  if ($trimmed_value === '') {
1338  continue;
1339  }
1340 
1341  if (preg_match("/^gap_(\d+)/", $key, $matches)) {
1342  $gap = $this->getGap($matches[1]);
1343  if (!is_object($gap)
1344  || $gap->getType() == CLOZE_SELECT && $trimmed_value == -1) {
1345  continue;
1346  }
1347 
1348  if ($gap->getType() == CLOZE_NUMERIC && !is_numeric(str_replace(",", ".", $trimmed_value))) {
1349  $trimmed_value = null;
1350  } elseif ($gap->getType() == CLOZE_NUMERIC) {
1351  $trimmed_value = str_replace(",", ".", $trimmed_value);
1352  }
1353  $solutionSubmit[trim($matches[1])] = $trimmed_value;
1354  }
1355  }
1356 
1357  return $solutionSubmit;
1358  }
1359 
1361  {
1362  $submit = $_POST;
1363  $solutionSubmit = array();
1364 
1365  foreach ($submit as $key => $value) {
1366  if (preg_match("/^gap_(\d+)/", $key, $matches)) {
1367  if ($value !== null && $value !== '') {
1368  $gap = $this->getGap($matches[1]);
1369  if (is_object($gap)) {
1370  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1))) {
1371  if ($gap->getType() == CLOZE_NUMERIC) {
1372  $value = str_replace(",", ".", $value);
1373  }
1374  $solutionSubmit[trim($matches[1])] = $value;
1375  }
1376  }
1377  }
1378  }
1379  }
1380 
1381  return $solutionSubmit;
1382  }
1383 
1384  public function getSolutionSubmit()
1385  {
1386  return $this->fetchSolutionSubmit($_POST);
1387  }
1388 
1397  public function saveWorkingData($active_id, $pass = null, $authorized = true)
1398  {
1399  global $DIC;
1400  $ilDB = $DIC['ilDB'];
1401  $ilUser = $DIC['ilUser'];
1402  if (is_null($pass)) {
1403  include_once "./Modules/Test/classes/class.ilObjTest.php";
1404  $pass = ilObjTest::_getPass($active_id);
1405  }
1406 
1407  $entered_values = 0;
1408 
1409  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
1410  $this->removeCurrentSolution($active_id, $pass, $authorized);
1411 
1412  foreach ($this->getSolutionSubmit() as $key => $value) {
1413  if ($value !== null && $value !== '') {
1414  $gap = $this->getGap($key);
1415  if (is_object($gap)) {
1416  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1))) {
1417  $this->saveCurrentSolution($active_id, $pass, $key, $value, $authorized);
1418  $entered_values++;
1419  }
1420  }
1421  }
1422  }
1423  });
1424 
1425  if ($entered_values) {
1426  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1428  assQuestion::logAction($this->lng->txtlng("assessment", "log_user_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
1429  }
1430  } else {
1431  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1433  assQuestion::logAction($this->lng->txtlng("assessment", "log_user_not_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
1434  }
1435  }
1436 
1437  return true;
1438  }
1439 
1446  public function getQuestionType()
1447  {
1448  return "assClozeTest";
1449  }
1450 
1458  public function getTextgapRating()
1459  {
1460  return $this->textgap_rating;
1461  }
1462 
1470  public function setTextgapRating($a_textgap_rating)
1471  {
1472  switch ($a_textgap_rating) {
1480  $this->textgap_rating = $a_textgap_rating;
1481  break;
1482  default:
1483  $this->textgap_rating = TEXTGAP_RATING_CASEINSENSITIVE;
1484  break;
1485  }
1486  }
1487 
1495  public function getIdenticalScoring()
1496  {
1497  return ($this->identical_scoring) ? 1 : 0;
1498  }
1499 
1507  public function setIdenticalScoring($a_identical_scoring)
1508  {
1509  $this->identical_scoring = ($a_identical_scoring) ? 1 : 0;
1510  }
1511 
1518  public function getAdditionalTableName()
1519  {
1520  return "qpl_qst_cloze";
1521  }
1522 
1529  public function getAnswerTableName()
1530  {
1531  return array("qpl_a_cloze",'qpl_a_cloze_combi_res');
1532  }
1533 
1540  public function setFixedTextLength($a_text_len)
1541  {
1542  $this->fixedTextLength = $a_text_len;
1543  }
1544 
1551  public function getFixedTextLength()
1552  {
1553  return $this->fixedTextLength;
1554  }
1555 
1564  public function getMaximumGapPoints($gap_index)
1565  {
1566  $points = 0;
1567  $gap_max_points = 0;
1568  if (array_key_exists($gap_index, $this->gaps)) {
1569  $gap = &$this->gaps[$gap_index];
1570  foreach ($gap->getItems($this->getShuffler()) as $answer) {
1571  if ($answer->getPoints() > $gap_max_points) {
1572  $gap_max_points = $answer->getPoints();
1573  }
1574  }
1575  $points += $gap_max_points;
1576  }
1577  return $points;
1578  }
1579 
1584  public function getRTETextWithMediaObjects()
1585  {
1586  return parent::getRTETextWithMediaObjects() . $this->getClozeText();
1587  }
1588  public function getGapCombinationsExists()
1589  {
1591  }
1592 
1593  public function getGapCombinations()
1594  {
1595  return $this->gap_combinations;
1596  }
1597 
1598  public function setGapCombinationsExists($value)
1599  {
1600  $this->gap_combinations_exists = $value;
1601  }
1602 
1603  public function setGapCombinations($value)
1604  {
1605  $this->gap_combinations = $value;
1606  }
1607 
1611  public function setExportDetailsXLS($worksheet, $startrow, $active_id, $pass)
1612  {
1613  parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
1614 
1615  $solution = $this->getSolutionValues($active_id, $pass);
1616  $i = 1;
1617  foreach ($this->getGaps() as $gap_index => $gap) {
1618  $worksheet->setCell($startrow + $i, 0, $this->lng->txt("gap") . " $i");
1619  $worksheet->setBold($worksheet->getColumnCoord(0) . ($startrow + $i));
1620  $checked = false;
1621  foreach ($solution as $solutionvalue) {
1622  if ($gap_index == $solutionvalue["value1"]) {
1623  $string_escaping_org_value = $worksheet->getStringEscaping();
1624  try {
1625  $worksheet->setStringEscaping(false);
1626 
1627  switch ($gap->getType()) {
1628  case CLOZE_SELECT:
1629  $worksheet->setCell($startrow + $i, 2, $gap->getItem($solutionvalue["value2"])->getAnswertext());
1630  break;
1631  case CLOZE_NUMERIC:
1632  case CLOZE_TEXT:
1633  $worksheet->setCell($startrow + $i, 2, $solutionvalue["value2"]);
1634  break;
1635  }
1636  } finally {
1637  $worksheet->setStringEscaping($string_escaping_org_value);
1638  }
1639  }
1640  }
1641  $i++;
1642  }
1643 
1644  return $startrow + $i + 1;
1645  }
1646 
1651  {
1652  // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1653  //$this->setClozeText( $migrator->migrateToLmContent($this->getClozeText()) );
1654  $this->cloze_text = $migrator->migrateToLmContent($this->getClozeText());
1655  // DO NOT USE SETTER FOR CLOZE TEXT -> SETTER DOES RECREATE GAP OBJECTS without having gap type info ^^
1656  }
1657 
1661  public function toJSON()
1662  {
1663  include_once("./Services/RTE/classes/class.ilRTE.php");
1664  $result = array();
1665  $result['id'] = (int) $this->getId();
1666  $result['type'] = (string) $this->getQuestionType();
1667  $result['title'] = (string) $this->getTitle();
1668  $result['question'] = $this->formatSAQuestion($this->getQuestion());
1669  $result['clozetext'] = $this->formatSAQuestion($this->getClozeText());
1670  $result['nr_of_tries'] = (int) $this->getNrOfTries();
1671  $result['shuffle'] = (bool) $this->getShuffle();
1672  $result['feedback'] = array(
1673  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1674  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1675  );
1676 
1677  $gaps = array();
1678  foreach ($this->getGaps() as $key => $gap) {
1679  $items = array();
1680  foreach ($gap->getItems($this->getShuffler()) as $item) {
1681  $jitem = array();
1682  $jitem['points'] = $item->getPoints();
1683  $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1684  $jitem['order'] = $item->getOrder();
1685  if ($gap->getType() == CLOZE_NUMERIC) {
1686  $jitem['lowerbound'] = $item->getLowerBound();
1687  $jitem['upperbound'] = $item->getUpperBound();
1688  } else {
1689  $jitem['value'] = trim($jitem['value']);
1690  }
1691  array_push($items, $jitem);
1692  }
1693 
1694  if ($gap->getGapSize() && ($gap->getType() == CLOZE_TEXT || $gap->getType() == CLOZE_NUMERIC)) {
1695  $jgap['size'] = $gap->getGapSize();
1696  }
1697 
1698  $jgap['shuffle'] = $gap->getShuffle();
1699  $jgap['type'] = $gap->getType();
1700  $jgap['item'] = $items;
1701 
1702  array_push($gaps, $jgap);
1703  }
1704  $result['gaps'] = $gaps;
1705  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1706  $result['mobs'] = $mobs;
1707  return json_encode($result);
1708  }
1709 
1718  public function getOperators($expression)
1719  {
1720  require_once "./Modules/TestQuestionPool/classes/class.ilOperatorsExpressionMapping.php";
1722  }
1723 
1728  public function getExpressionTypes()
1729  {
1730  return array(
1736  );
1737  }
1738 
1747  public function getUserQuestionResult($active_id, $pass)
1748  {
1750  global $DIC;
1751  $ilDB = $DIC['ilDB'];
1752  $result = new ilUserQuestionResult($this, $active_id, $pass);
1753 
1754  $maxStep = $this->lookupMaxStep($active_id, $pass);
1755 
1756  if ($maxStep !== null) {
1757  $data = $ilDB->queryF(
1758  "
1759  SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1760  FROM tst_solutions sol
1761  INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1762  WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s AND sol.step = %s
1763  GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1764  ",
1765  array("integer", "integer", "integer","integer"),
1766  array($active_id, $pass, $this->getId(), $maxStep)
1767  );
1768  } else {
1769  $data = $ilDB->queryF(
1770  "
1771  SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type
1772  FROM tst_solutions sol
1773  INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi
1774  WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s
1775  GROUP BY sol.solution_id, sol.value1+1, sol.value2, cloze.cloze_type
1776  ",
1777  array("integer", "integer", "integer"),
1778  array($active_id, $pass, $this->getId())
1779  );
1780  }
1781 
1782  while ($row = $ilDB->fetchAssoc($data)) {
1783  if ($row["cloze_type"] == 1) {
1784  $row["value2"]++;
1785  }
1786  $result->addKeyValue($row["val"], $row["value2"]);
1787  }
1788 
1789  $points = $this->calculateReachedPoints($active_id, $pass);
1790  $max_points = $this->getMaximumPoints();
1791 
1792  $result->setReachedPercentage(($points / $max_points) * 100);
1793 
1794  return $result;
1795  }
1796 
1805  public function getAvailableAnswerOptions($index = null)
1806  {
1807  if ($index !== null) {
1808  return $this->getGap($index);
1809  } else {
1810  return $this->getGaps();
1811  }
1812  }
1813 
1814  public function calculateCombinationResult($user_result)
1815  {
1816  $points = 0;
1817 
1818  $assClozeGapCombinationObj = new assClozeGapCombination();
1819 
1820  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1821  $combinations_for_question = $assClozeGapCombinationObj->getCleanCombinationArray($this->getId());
1822  $gap_answers = array();
1823  $gap_used_in_combination = array();
1824  foreach ($user_result as $user_result_build_list) {
1825  if (is_array($user_result_build_list)) {
1826  $gap_answers[$user_result_build_list['gap_id']] = $user_result_build_list['value'];
1827  }
1828  }
1829 
1830  foreach ($combinations_for_question as $combination) {
1831  foreach ($combination as $row_key => $row_answers) {
1832  $combination_fulfilled = true;
1833  $points_for_combination = $row_answers['points'];
1834  foreach ($row_answers as $gap_key => $combination_gap_answer) {
1835  if ($gap_key !== 'points') {
1836  $gap_used_in_combination[$gap_key] = $gap_key;
1837  }
1838  if ($combination_fulfilled && array_key_exists($gap_key, $gap_answers)) {
1839  switch ($combination_gap_answer['type']) {
1840  case CLOZE_TEXT:
1841  $is_text_gap_correct = $this->getTextgapPoints($gap_answers[$gap_key], $combination_gap_answer['answer'], 1);
1842  if ($is_text_gap_correct != 1) {
1843  $combination_fulfilled = false;
1844  }
1845  break;
1846  case CLOZE_SELECT:
1847  $answer = $this->gaps[$gap_key]->getItem($gap_answers[$gap_key]);
1848  $answertext = $answer->getAnswertext();
1849  if ($answertext != $combination_gap_answer['answer']) {
1850  $combination_fulfilled = false;
1851  }
1852  break;
1853  case CLOZE_NUMERIC:
1854  $answer = $this->gaps[$gap_key]->getItem(0);
1855  if ($combination_gap_answer['answer'] != 'out_of_bound') {
1856  $is_numeric_gap_correct = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1857  if ($is_numeric_gap_correct != 1) {
1858  $combination_fulfilled = false;
1859  }
1860  } else {
1861  $wrong_is_the_new_right = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1862  if ($wrong_is_the_new_right == 1) {
1863  $combination_fulfilled = false;
1864  }
1865  }
1866  break;
1867  }
1868  } else {
1869  if ($gap_key !== 'points') {
1870  $combination_fulfilled = false;
1871  }
1872  }
1873  }
1874  if ($combination_fulfilled) {
1875  $points += $points_for_combination;
1876  }
1877  }
1878  }
1879  }
1880  return array($points, $gap_used_in_combination);
1881  }
1887  protected function calculateReachedPointsForSolution($user_result, &$detailed = null)
1888  {
1889  if ($detailed === null) {
1890  $detailed = array();
1891  }
1892 
1893  $assClozeGapCombinationObj = new assClozeGapCombination();
1894  $combinations[1] = array();
1895  if ($assClozeGapCombinationObj->combinationExistsForQid($this->getId())) {
1896  $combinations = $this->calculateCombinationResult($user_result);
1897  $points = $combinations[0];
1898  }
1899  $counter = 0;
1900  $solution_values_text = array(); // for identical scoring checks
1901  $solution_values_select = array(); // for identical scoring checks
1902  $solution_values_numeric = array(); // for identical scoring checks
1903  foreach ($user_result as $gap_id => $value) {
1904  if (is_string($value)) {
1905  $value = array("value" => $value);
1906  }
1907 
1908  if (array_key_exists($gap_id, $this->gaps) && !array_key_exists($gap_id, $combinations[1])) {
1909  switch ($this->gaps[$gap_id]->getType()) {
1910  case CLOZE_TEXT:
1911  $gappoints = 0;
1912  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1913  $answer = $this->gaps[$gap_id]->getItem($order);
1914  $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1915  if ($gotpoints > $gappoints) {
1916  $gappoints = $gotpoints;
1917  }
1918  }
1919  if (!$this->getIdenticalScoring()) {
1920  // check if the same solution text was already entered
1921  if ((in_array($value["value"], $solution_values_text)) && ($gappoints > 0)) {
1922  $gappoints = 0;
1923  }
1924  }
1925  $points += $gappoints;
1926  $detailed[$gap_id] = array("points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0) ? true : false);
1927  array_push($solution_values_text, $value["value"]);
1928  break;
1929  case CLOZE_NUMERIC:
1930  $gappoints = 0;
1931  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1932  $answer = $this->gaps[$gap_id]->getItem($order);
1933  $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1934  if ($gotpoints > $gappoints) {
1935  $gappoints = $gotpoints;
1936  }
1937  }
1938  if (!$this->getIdenticalScoring()) {
1939  // check if the same solution value was already entered
1940  include_once "./Services/Math/classes/class.EvalMath.php";
1941  $eval = new EvalMath();
1942  $eval->suppress_errors = true;
1943  $found_value = false;
1944  foreach ($solution_values_numeric as $solval) {
1945  if ($eval->e($solval) == $eval->e($value["value"])) {
1946  $found_value = true;
1947  }
1948  }
1949  if ($found_value && ($gappoints > 0)) {
1950  $gappoints = 0;
1951  }
1952  }
1953  $points += $gappoints;
1954  $detailed[$gap_id] = array("points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? true : false, "positive" => ($gappoints > 0) ? true : false);
1955  array_push($solution_values_numeric, $value["value"]);
1956  break;
1957  case CLOZE_SELECT:
1958  if ($value["value"] >= 0) {
1959  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++) {
1960  $answer = $this->gaps[$gap_id]->getItem($order);
1961  if ($value["value"] == $answer->getOrder()) {
1962  $answerpoints = $answer->getPoints();
1963  if (!$this->getIdenticalScoring()) {
1964  // check if the same solution value was already entered
1965  if ((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0)) {
1966  $answerpoints = 0;
1967  }
1968  }
1969  $points += $answerpoints;
1970  $detailed[$gap_id] = array("points" => $answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? true : false, "positive" => ($answerpoints > 0) ? true : false);
1971  array_push($solution_values_select, $answer->getAnswertext());
1972  }
1973  }
1974  }
1975  break;
1976  }
1977  }
1978  }
1979 
1980  return $points;
1981  }
1982 
1984  {
1985  $userSolution = array();
1986 
1987  foreach ($previewSession->getParticipantsSolution() as $key => $val) {
1988  $userSolution[$key] = array('gap_id' => $key, 'value' => $val);
1989  }
1990 
1991  $reachedPoints = $this->calculateReachedPointsForSolution($userSolution);
1992  $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $reachedPoints);
1993 
1994  return $this->ensureNonNegativePoints($reachedPoints);
1995  }
1996 
1997  public function fetchAnswerValueForGap($userSolution, $gapIndex)
1998  {
1999  $answerValue = '';
2000 
2001  foreach ($userSolution as $value1 => $value2) {
2002  if ($value1 == $gapIndex) {
2003  $answerValue = $value2;
2004  break;
2005  }
2006  }
2007 
2008  return $answerValue;
2009  }
2010 
2011  public function isAddableAnswerOptionValue($qIndex, $answerOptionValue)
2012  {
2013  $gap = $this->getGap($qIndex);
2014 
2015  if ($gap->getType() != CLOZE_TEXT) {
2016  return false;
2017  }
2018 
2019  foreach ($gap->getItems(new ilArrayElementOrderKeeper()) as $item) {
2020  if ($item->getAnswertext() === $answerOptionValue) {
2021  return false;
2022  }
2023  }
2024 
2025  return true;
2026  }
2027 
2028  public function addAnswerOptionValue($qIndex, $answerOptionValue, $points)
2029  {
2030  $gap = $this->getGap($qIndex); /* @var assClozeGap $gap */
2031 
2032  $item = new assAnswerCloze($answerOptionValue, $points);
2033  $item->setOrder($gap->getItemCount());
2034 
2035  $gap->addItem($item);
2036  }
2037 
2038  public function savePartial()
2039  {
2040  return true;
2041  }
2042 }
static logAction($logtext="", $active_id="", $question_id="")
Logs an action into the Test&Assessment log.
getId()
Gets the id of the assQuestion object.
fetchSolutionSubmit($submit)
toJSON()
Returns a JSON representation of the question.
static _getMobsOfObject($a_type, $a_id, $a_usage_hist_nr=0, $a_lang="-")
get mobs of object
getGapCount()
Returns the number of gaps.
static _getOriginalId($question_id)
Returns the original id of a question.
formatSAQuestion($a_q)
Format self assessment question.
const TEXTGAP_RATING_LEVENSHTEIN5
Class for cloze question numeric answers.
$size
Definition: RandomTest.php:84
getMaximumGapPoints($gap_index)
Returns the maximum points for a gap.
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
getQuestionType()
Returns the question type of the question.
Class iQuestionCondition.
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
$data
Definition: storeScorm.php:23
getClozeTextForHTMLOutput()
Returns the cloze text as HTML (with optional nl2br) Fix for Mantis 29987: We assume Tiny embeds any ...
setEndTag($end_tag="[/gap]")
Sets the end tag of a cloze gap.
clearGapAnswers()
Removes all answers from the gaps.
Class for cloze tests.
getAdditionalTableName()
Returns the name of the additional question data table in the database.
saveAnswerSpecificDataToDb()
Save all gaps to the database.
getAnswerTableName()
Returns the name of the answer table in the database.
$result
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
$mobs
Definition: imgupload.php:54
const TEXTGAP_RATING_LEVENSHTEIN2
const TEXTGAP_RATING_LEVENSHTEIN1
Abstract basic class which is to be extended by the concrete assessment question type classes...
const CLOZE_TEXT
Cloze question constants.
setGapSize($gap_index, $order, $size)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
calculateCombinationResult($user_result)
getOperators($expression)
Get all available operations for a specific question.
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
ensureNonNegativePoints($points)
getSolutionValues($active_id, $pass=null, $authorized=true)
Loads solutions of a given user from the database an returns it.
setId($id=-1)
Sets the id of the assQuestion object.
getSolutionMaxPass($active_id)
Returns the maximum pass a users question solution.
setEstimatedWorkingTime($hour=0, $min=0, $sec=0)
Sets the estimated working time of a question from given hour, minute and second. ...
copyGapCombination($orgID, $newID)
getGap($gap_index=0)
Returns the gap at a given index.
setGapAnswerPoints($gap_index, $order, $points)
Sets the points of a gap with a given index and an answer with a given order.
getTextgapRating()
Returns the rating option for text gaps.
Class for cloze question gaps.
saveToDb($original_id="")
Saves a assClozeTest object to a database.
static strToLower($a_string)
Definition: class.ilStr.php:87
setClozeTextValue($cloze_text="")
getUserQuestionResult($active_id, $pass)
Get the user solution for a question by active_id and the test pass.
checkForValidFormula($value)
setNrOfTries($a_nr_of_tries)
setExportDetailsXLS($worksheet, $startrow, $active_id, $pass)
{}
setAdditionalContentEditingMode($additinalContentEditingMode)
setter for additional content editing mode for this question
static _replaceMediaObjectImageSrc($a_text, $a_direction=0, $nic=IL_INST_ID)
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
createGapsFromQuestiontext()
Create gap entries by parsing the question text.
getObjId()
Get the object id of the container object.
getShuffle()
Gets the shuffle flag.
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...
addAnswerOptionValue($qIndex, $answerOptionValue, $points)
$index
Definition: metadata.php:128
getStartTag()
Returns the start tag of a cloze gap.
isAddableAnswerOptionValue($qIndex, $answerOptionValue)
getTextgapPoints($a_original, $a_entered, $max_points)
Returns the points for a text gap and compares the given solution with the entered solution using the...
setGapType($gap_index, $gap_type)
Set the type of a gap with a given index.
setGapCombinationsExists($value)
static _getLogLanguage()
retrieve the log language for assessment logging
getGaps()
Returns the array of gaps.
setAuthor($author="")
Sets the authors name of the assQuestion object.
duplicate($for_test=true, $title="", $author="", $owner="", $testObjId=null)
Duplicates an assClozeTest.
static _enabledAssessmentLogging()
check wether assessment logging is enabled or not
const CLOZE_SELECT
getAuthor()
Gets the authors name of the assQuestion object.
const TEXTGAP_RATING_LEVENSHTEIN3
setGapAnswerLowerBound($gap_index, $order, $bound)
Sets the lower bound of a gap with a given index and an answer with a given order.
Class ilUserQuestionResult.
saveCurrentSolution($active_id, $pass, $value1, $value2, $authorized=true, $tstamp=null)
setFixedTextLength($a_text_len)
Sets a fixed text length for all text fields in the cloze question.
saveWorkingData($active_id, $pass=null, $authorized=true)
Saves the learners input of the question to the database.
isValidNumericSubmitValue($submittedValue)
copyObject($target_questionpool_id, $title="")
Copies an assClozeTest object.
const TEXTGAP_RATING_CASESENSITIVE
Interface ilObjAnswerScoringAdjustable.
global $DIC
Definition: goto.php:24
getQuestion()
Gets the question string of the question object.
calculateReachedPoints($active_id, $pass=null, $authorized=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
$query
addGapAtIndex($gap, $index)
Adds a ClozeGap object at a given index.
setGapAnswerUpperBound($gap_index, $order, $bound)
Sets the upper bound of a gap with a given index and an answer with a given order.
saveClozeTextGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
saveAdditionalQuestionDataToDb()
Saves the data for the additional data table.
addGapAnswer($gap_index, $order, $answer)
Sets the answer text of a gap with a given index.
setGapShuffle($gap_index=0, $shuffle=1)
Sets the shuffle state of a gap with a given index.
setClozeText($cloze_text="")
Evaluates the text gap solutions from the cloze text.
cleanQuestiontext($text)
Cleans cloze question text to remove attributes or tags from older ILIAS versions.
getExpressionTypes()
Get all available expression types for a specific question.
fetchAnswerValueForGap($userSolution, $gapIndex)
flushGaps()
Deletes all gaps without changing the cloze text.
static sendFailure($a_info="", $a_keep=false)
Send Failure Message to Screen.
isAdditionalContentEditingModePageObject()
isser for additional "pageobject" content editing mode
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
setPoints($a_points)
Sets the maximum available points for the question.
saveQuestionDataToDb($original_id="")
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession)
saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
deleteGap($gap_index)
Deletes a gap with a given index.
replaceFirstGap($gaptext, $content)
Replace the first gap in a string without treating backreferences.
updateClozeTextFromGaps()
Updates the gap parameters in the cloze text from the form input.
setFeedbackMode($feedbackMode)
setTextgapRating($a_textgap_rating)
Sets the rating option for text gaps.
setQuestion($question="")
Sets the question string of the question object.
Interface ilObjQuestionScoringAdjustable.
getEndTag()
Returns the end tag of a cloze gap.
removeCurrentSolution($active_id, $pass, $authorized=true)
setStartTag($start_tag="[gap]")
Sets the start tag of a cloze gap.
__construct(Container $dic, ilPlugin $plugin)
addGapText($gap_index)
Adds a new answer text value to a text gap with a given index.
prepareTextareaOutput($txt_output, $prepare_for_latex_output=false, $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output in tests.
deleteAnswerText($gap_index, $answer_index)
Deletes the answer text of a gap with a given index and an answer with a given order.
getFixedTextLength()
Gets the fixed text length for all text fields in the cloze question.
const TEXTGAP_RATING_LEVENSHTEIN4
global $ilDB
setOriginalId($original_id)
getCurrentSolutionResultSet($active_id, $pass, $authorized=true)
Get a restulset for the current user solution for a this question by active_id and pass...
setLifecycle(ilAssQuestionLifecycle $lifecycle)
saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap)
Saves a gap-item record.
getClozeText()
Returns the cloze text.
getTitle()
Gets the title string of the assQuestion object.
__construct( $title="", $comment="", $author="", $owner=-1, $question="")
assClozeTest constructor
loadFromDb($question_id)
Loads a assClozeTest object from a database.
$ilUser
Definition: imgupload.php:18
const CLOZE_NUMERIC
isComplete()
Returns TRUE, if a cloze test is complete for use.
getIdenticalScoring()
Returns the identical scoring status of the question.
setTitle($title="")
Sets the title string of the assQuestion object.
setObjId($obj_id=0)
Set the object id of the container object.
setIdenticalScoring($a_identical_scoring)
Sets the identical scoring option for cloze questions.
setComment($comment="")
Sets the comment string of the assQuestion object.
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
$_POST["username"]
const FB_MODE_GAP_QUESTION
constants for different feedback modes (per gap or per gap-answers/options)
saveClozeGapItemsToDb($gap, $key)
Save all items belonging to one cloze gap to the db.
calculateReachedPointsForSolution($user_result, &$detailed=null)
$i
Definition: metadata.php:24
setOwner($owner="")
Sets the creator/owner ID of the assQuestion object.
const TEXTGAP_RATING_CASEINSENSITIVE