ILIAS  Release_5_0_x_branch Revision 61816
 All Data Structures Namespaces Files Functions Variables Groups 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 
24 {
32  var $gaps;
33 
42 
43 
45 
54 
62  var $end_tag;
63 
75 
86 
93 
94  public $cloze_text;
95 
109  function __construct(
110  $title = "",
111  $comment = "",
112  $author = "",
113  $owner = -1,
114  $question = ""
115  )
116  {
118  $this->start_tag = "[gap]";
119  $this->end_tag = "[/gap]";
120  $this->gaps = array();
121  $this->setQuestion($question); // @TODO: Should this be $question?? See setter for why this is not trivial.
122  $this->fixedTextLength = "";
123  $this->identical_scoring = 1;
124  $this->gap_combinations_exists = false;
125  $this->gap_combinations = array();
126  }
127 
133  public function isComplete()
134  {
135  if (strlen($this->getTitle())
136  && $this->getAuthor()
137  && $this->getClozeText()
138  && count($this->getGaps())
139  && $this->getMaximumPoints() > 0)
140  {
141  return true;
142  }
143  return false;
144  }
145 
153  public function cleanQuestiontext($text)
154  {
155  $text = preg_replace("/\[gap[^\]]*?\]/", "[gap]", $text);
156  $text = preg_replace("/<gap([^>]*?)>/", "[gap]", $text);
157  $text = str_replace("</gap>", "[/gap]", $text);
158  return $text;
159  }
160 
167  public function loadFromDb($question_id)
168  {
169  global $ilDB;
170  $result = $ilDB->queryF("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",
171  array("integer"),
172  array($question_id)
173  );
174  if ($result->numRows() == 1)
175  {
176  $data = $ilDB->fetchAssoc($result);
177  $this->setId($question_id);
178  $this->setNrOfTries($data['nr_of_tries']);
179  $this->setObjId($data["obj_fi"]);
180  $this->setTitle($data["title"]);
181  $this->setComment($data["description"]);
182  $this->setOriginalId($data["original_id"]);
183  $this->setAuthor($data["author"]);
184  $this->setPoints($data["points"]);
185  $this->setOwner($data["owner"]);
186  $this->setQuestion($this->cleanQuestiontext($data["question_text"]));
187  $this->setClozeText($data['cloze_text']);
188  $this->setFixedTextLength($data["fixed_textlen"]);
189  $this->setIdenticalScoring(($data['tstamp'] == 0) ? true : $data["identical_scoring"]);
190  // replacement of old syntax with new syntax
191  include_once("./Services/RTE/classes/class.ilRTE.php");
192  $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1);
193  $this->cloze_text = ilRTE::_replaceMediaObjectImageSrc($this->cloze_text, 1);
194  $this->setTextgapRating($data["textgap_rating"]);
195  $this->setEstimatedWorkingTime(substr($data["working_time"], 0, 2), substr($data["working_time"], 3, 2), substr($data["working_time"], 6, 2));
196 
197  try
198  {
199  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
200  }
202  {
203  }
204 
205  // open the cloze gaps with all answers
206  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
207  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
208  $result = $ilDB->queryF("SELECT * FROM qpl_a_cloze WHERE question_fi = %s ORDER BY gap_id, aorder ASC",
209  array("integer"),
210  array($question_id)
211  );
212  if ($result->numRows() > 0)
213  {
214  $this->gaps = array();
215  while ($data = $ilDB->fetchAssoc($result))
216  {
217  switch ($data["cloze_type"])
218  {
219  case CLOZE_TEXT:
220  if (!array_key_exists($data["gap_id"], $this->gaps))
221  {
222  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_TEXT);
223  }
224  $answer = new assAnswerCloze(
225  $data["answertext"],
226  $data["points"],
227  $data["aorder"]
228  );
229  $this->gaps[$data["gap_id"]]->setGapSize($data['gap_size']);
230 
231  $this->gaps[$data["gap_id"]]->addItem($answer);
232  break;
233  case CLOZE_SELECT:
234  if (!array_key_exists($data["gap_id"], $this->gaps))
235  {
236  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_SELECT);
237  $this->gaps[$data["gap_id"]]->setShuffle($data["shuffle"]);
238  }
239  $answer = new assAnswerCloze(
240  $data["answertext"],
241  $data["points"],
242  $data["aorder"]
243  );
244  $this->gaps[$data["gap_id"]]->addItem($answer);
245  break;
246  case CLOZE_NUMERIC:
247  if (!array_key_exists($data["gap_id"], $this->gaps))
248  {
249  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_NUMERIC);
250  }
251  $answer = new assAnswerCloze(
252  $data["answertext"],
253  $data["points"],
254  $data["aorder"]
255  );
256  $this->gaps[$data["gap_id"]]->setGapSize($data['gap_size']);
257  $answer->setLowerBound($data["lowerlimit"]);
258  $answer->setUpperBound($data["upperlimit"]);
259  $this->gaps[$data["gap_id"]]->addItem($answer);
260  break;
261  }
262  }
263  }
264  }
265  $assClozeGapCombinationObj = new assClozeGapCombination();
266  $check_for_gap_combinations = $assClozeGapCombinationObj->loadFromDb($question_id);
267  if(count($check_for_gap_combinations) != 0)
268  {
269  $this->setGapCombinationsExists(true);
270  $this->setGapCombinations($check_for_gap_combinations);
271  }
272  parent::loadFromDb($question_id);
273  }
274 
275  #region Save question to db
276 
286  public function saveToDb($original_id = "")
287  {
291 
293  }
294 
298  public function saveAnswerSpecificDataToDb()
299  {
300  global $ilDB;
301 
302  $ilDB->manipulateF( "DELETE FROM qpl_a_cloze WHERE question_fi = %s",
303  array( "integer" ),
304  array( $this->getId() )
305  );
306 
307  foreach ($this->gaps as $key => $gap)
308  {
309  $this->saveClozeGapItemsToDb( $gap, $key );
310  }
311  }
312 
319  {
320  global $ilDB;
321 
322  $ilDB->manipulateF( "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
323  array( "integer" ),
324  array( $this->getId() )
325  );
326 
327  $ilDB->manipulateF( "INSERT INTO " . $this->getAdditionalTableName()
328  . " (question_fi, textgap_rating, identical_scoring, fixed_textlen, cloze_text) VALUES (%s, %s, %s, %s, %s)",
329  array(
330  "integer",
331  "text",
332  "text",
333  "integer",
334  "text"
335  ),
336  array(
337  $this->getId(),
338  $this->getTextgapRating(),
339  $this->getIdenticalScoring(),
340  $this->getFixedTextLength() ? $this->getFixedTextLength() : NULL,
342  )
343  );
344  }
345 
352  protected function saveClozeGapItemsToDb($gap, $key)
353  {
354  global $ilDB;
355  foreach ($gap->getItems() as $item)
356  {
357  $query = "";
358  $next_id = $ilDB->nextId( 'qpl_a_cloze' );
359  switch ($gap->getType())
360  {
361  case CLOZE_TEXT:
362  $this->saveClozeTextGapRecordToDb($next_id, $key, $item, $gap );
363  break;
364  case CLOZE_SELECT:
365  $this->saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap );
366  break;
367  case CLOZE_NUMERIC:
368  $this->saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap );
369  break;
370  }
371  }
372  }
373 
382  protected function saveClozeTextGapRecordToDb($next_id, $key, $item, $gap)
383  {
384  global $ilDB;
385  $ilDB->manipulateF( "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)",
386  array(
387  "integer",
388  "integer",
389  "integer",
390  "text",
391  "float",
392  "integer",
393  "text",
394  "integer"
395  ),
396  array(
397  $next_id,
398  $this->getId(),
399  $key,
400  strlen( $item->getAnswertext() ) ? $item->getAnswertext() : "",
401  $item->getPoints(),
402  $item->getOrder(),
403  $gap->getType(),
404  $gap->getGapSize()
405  )
406  );
407  }
408 
417  protected function saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap)
418  {
419  global $ilDB;
420  $ilDB->manipulateF( "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)",
421  array(
422  "integer",
423  "integer",
424  "integer",
425  "text",
426  "float",
427  "integer",
428  "text",
429  "text"
430  ),
431  array(
432  $next_id,
433  $this->getId(),
434  $key,
435  strlen( $item->getAnswertext() ) ? $item->getAnswertext() : "",
436  $item->getPoints(),
437  $item->getOrder(),
438  $gap->getType(),
439  ($gap->getShuffle()) ? "1" : "0"
440  )
441  );
442  }
443 
452  protected function saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap)
453  {
454  global $ilDB;
455 
456  include_once "./Services/Math/classes/class.EvalMath.php";
457  $eval = new EvalMath();
458  $eval->suppress_errors = TRUE;
459  $ilDB->manipulateF( "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)",
460  array(
461  "integer",
462  "integer",
463  "integer",
464  "text",
465  "float",
466  "integer",
467  "text",
468  "text",
469  "text",
470  "integer"
471  ),
472  array(
473  $next_id,
474  $this->getId(),
475  $key,
476  strlen( $item->getAnswertext() ) ? $item->getAnswertext() : "",
477  $item->getPoints(),
478  $item->getOrder(),
479  $gap->getType(),
480  ($eval->e( $item->getLowerBound() !== FALSE ) && strlen( $item->getLowerBound()
481  ) > 0) ? $item->getLowerBound() : $item->getAnswertext(),
482  ($eval->e( $item->getUpperBound() !== FALSE ) && strlen( $item->getUpperBound()
483  ) > 0) ? $item->getUpperBound() : $item->getAnswertext(),
484  $gap->getGapSize()
485  )
486  );
487  }
488 
489 
490 
491  #endregion Save question to db
492 
499  function getGaps()
500  {
501  return $this->gaps;
502  }
503 
504 
511  function flushGaps()
512  {
513  $this->gaps = array();
514  }
515 
525  function setClozeText($cloze_text = "")
526  {
527  $this->gaps = array();
529  $this->cloze_text = $cloze_text;
531  }
532 
534  {
535  $this->cloze_text = $cloze_text;
536  }
537 
545  function getClozeText()
546  {
547  return $this->cloze_text;
548  }
549 
557  function getStartTag()
558  {
559  return $this->start_tag;
560  }
561 
569  function setStartTag($start_tag = "[gap]")
570  {
571  $this->start_tag = $start_tag;
572  }
573 
581  function getEndTag()
582  {
583  return $this->end_tag;
584  }
585 
593  function setEndTag($end_tag = "[/gap]")
594  {
595  $this->end_tag = $end_tag;
596  }
597 
605  {
606  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
607  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
608  $search_pattern = "|\[gap\](.*?)\[/gap\]|i";
609  preg_match_all($search_pattern, $this->getClozeText(), $found);
610  $this->gaps = array();
611  if (count($found[0]))
612  {
613  foreach ($found[1] as $gap_index => $answers)
614  {
615  // create text gaps by default
616  $gap = new assClozeGap(CLOZE_TEXT);
617  $textparams = preg_split("/(?<!\\\\),/", $answers);
618  foreach ($textparams as $key => $value)
619  {
620  $answer = new assAnswerCloze($value, 0, $key);
621  $gap->addItem($answer);
622  }
623  $this->gaps[$gap_index] = $gap;
624  }
625  }
626  }
627 
633  function setGapType($gap_index, $gap_type)
634  {
635  if (array_key_exists($gap_index, $this->gaps))
636  {
637  $this->gaps[$gap_index]->setType($gap_type);
638  }
639  }
640 
650  function setGapShuffle($gap_index = 0, $shuffle = 1)
651  {
652  if (array_key_exists($gap_index, $this->gaps))
653  {
654  $this->gaps[$gap_index]->setShuffle($shuffle);
655  }
656  }
657 
664  function clearGapAnswers()
665  {
666  foreach ($this->gaps as $gap_index => $gap)
667  {
668  $this->gaps[$gap_index]->clearItems();
669  }
670  }
671 
679  function getGapCount()
680  {
681  if (is_array($this->gaps))
682  {
683  return count($this->gaps);
684  }
685  else
686  {
687  return 0;
688  }
689  }
690 
701  function addGapAnswer($gap_index, $order, $answer)
702  {
703  if (array_key_exists($gap_index, $this->gaps))
704  {
705  if ($this->gaps[$gap_index]->getType() == CLOZE_NUMERIC)
706  {
707  // only allow notation with "." for real numbers
708  $answer = str_replace(",", ".", $answer);
709  }
710  $this->gaps[$gap_index]->addItem(new assAnswerCloze($answer, 0, $order));
711  }
712  }
713 
722  function getGap($gap_index = 0)
723  {
724  if (array_key_exists($gap_index, $this->gaps))
725  {
726  return $this->gaps[$gap_index];
727  }
728  else
729  {
730  return NULL;
731  }
732  }
733 
734  public function setGapSize($gap_index, $order, $size)
735  {
736  if (array_key_exists($gap_index, $this->gaps))
737  {
738  $this->gaps[$gap_index]->setGapSize( $size);
739  }
740  }
741 
752  function setGapAnswerPoints($gap_index, $order, $points)
753  {
754  if (array_key_exists($gap_index, $this->gaps))
755  {
756  $this->gaps[$gap_index]->setItemPoints($order, $points);
757  }
758  }
759 
768  function addGapText($gap_index)
769  {
770  if (array_key_exists($gap_index, $this->gaps))
771  {
772  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
773  $answer = new assAnswerCloze(
774  "",
775  0,
776  $this->gaps[$gap_index]->getItemCount()
777  );
778  $this->gaps[$gap_index]->addItem($answer);
779  }
780  }
781 
790  function addGapAtIndex($gap, $index)
791  {
792  $this->gaps[$index] = $gap;
793  }
794 
805  function setGapAnswerLowerBound($gap_index, $order, $bound)
806  {
807  if (array_key_exists($gap_index, $this->gaps))
808  {
809  $this->gaps[$gap_index]->setItemLowerBound($order, $bound);
810  }
811  }
812 
823  function setGapAnswerUpperBound($gap_index, $order, $bound)
824  {
825  if (array_key_exists($gap_index, $this->gaps))
826  {
827  $this->gaps[$gap_index]->setItemUpperBound($order, $bound);
828  }
829  }
830 
837  function getMaximumPoints()
838  {
839  $assClozeGapCombinationObj = new assClozeGapCombination();
840  $points = 0;
841  $gaps_used_in_combination = array();
842  if($assClozeGapCombinationObj->combinationExistsForQid($this->getId()))
843  {
844  $points = $assClozeGapCombinationObj->getMaxPointsForCombination($this->getId());
845  $gaps_used_in_combination = $assClozeGapCombinationObj->getGapsWhichAreUsedInCombination($this->getId());
846  }
847  foreach ($this->gaps as $gap_index => $gap)
848  {
849  if(! array_key_exists($gap_index, $gaps_used_in_combination))
850  {
851  if ($gap->getType() == CLOZE_TEXT)
852  {
853  $gap_max_points = 0;
854  foreach ($gap->getItems() as $item)
855  {
856  if ($item->getPoints() > $gap_max_points)
857  {
858  $gap_max_points = $item->getPoints();
859  }
860  }
861  $points += $gap_max_points;
862  }
863  else if ($gap->getType() == CLOZE_SELECT)
864  {
865  $srpoints = 0;
866  foreach ($gap->getItems() as $item)
867  {
868  if ($item->getPoints() > $srpoints)
869  {
870  $srpoints = $item->getPoints();
871  }
872  }
873  $points += $srpoints;
874  }
875  else if ($gap->getType() == CLOZE_NUMERIC)
876  {
877  $numpoints = 0;
878  foreach ($gap->getItems() as $item)
879  {
880  if ($item->getPoints() > $numpoints)
881  {
882  $numpoints = $item->getPoints();
883  }
884  }
885  $points += $numpoints;
886  }
887  }
888  }
889 
890  return $points;
891  }
892 
898  function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null)
899  {
900  if ($this->id <= 0)
901  {
902  // The question has not been saved. It cannot be duplicated
903  return;
904  }
905  // duplicate the question in database
906  $this_id = $this->getId();
907  $thisObjId = $this->getObjId();
908 
909  $clone = $this;
910  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
912  $clone->id = -1;
913 
914  if( (int)$testObjId > 0 )
915  {
916  $clone->setObjId($testObjId);
917  }
918 
919  if ($title)
920  {
921  $clone->setTitle($title);
922  }
923  if ($author)
924  {
925  $clone->setAuthor($author);
926  }
927  if ($owner)
928  {
929  $clone->setOwner($owner);
930  }
931  if ($for_test)
932  {
933  $clone->saveToDb($original_id);
934  }
935  else
936  {
937  $clone->saveToDb();
938  }
939  if($this->gap_combinations_exists)
940  {
941  $this->copyGapCombination($this_id, $clone->getId());
942  }
943  if ($for_test)
944  {
945  $clone->saveToDb($original_id);
946  }
947  else
948  {
949  $clone->saveToDb();
950  }
951  // copy question page content
952  $clone->copyPageOfQuestion($this_id);
953  // copy XHTML media objects
954  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
955 
956  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
957 
958  return $clone->getId();
959  }
960 
966  function copyObject($target_questionpool_id, $title = "")
967  {
968  if ($this->getId() <= 0)
969  {
970  // The question has not been saved. It cannot be duplicated
971  return;
972  }
973 
974  $thisId = $this->getId();
975  $thisObjId = $this->getObjId();
976 
977  $clone = $this;
978  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
980  $clone->id = -1;
981  $clone->setObjId($target_questionpool_id);
982  if ($title)
983  {
984  $clone->setTitle($title);
985  }
986  if($this->gap_combinations_exists)
987  {
988  $this->copyGapCombination($original_id, $clone->getId());
989  }
990  $clone->saveToDb();
991 
992  // copy question page content
993  $clone->copyPageOfQuestion($original_id);
994  // copy XHTML media objects
995  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
996 
997  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
998 
999  return $clone->getId();
1000  }
1001 
1002  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "")
1003  {
1004  if ($this->id <= 0)
1005  {
1006  // The question has not been saved. It cannot be duplicated
1007  return;
1008  }
1009 
1010  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
1011 
1012  $sourceQuestionId = $this->id;
1013  $sourceParentId = $this->getObjId();
1014 
1015  // duplicate the question in database
1016  $clone = $this;
1017  $clone->id = -1;
1018 
1019  $clone->setObjId($targetParentId);
1020 
1021  if ($targetQuestionTitle)
1022  {
1023  $clone->setTitle($targetQuestionTitle);
1024  }
1025 
1026  $clone->saveToDb();
1027 
1028  if($this->gap_combinations_exists)
1029  {
1030  $this->copyGapCombination($sourceQuestionId, $clone->getId());
1031  }
1032  // copy question page content
1033  $clone->copyPageOfQuestion($sourceQuestionId);
1034  // copy XHTML media objects
1035  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
1036 
1037  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
1038 
1039  return $clone->id;
1040  }
1041 
1042  function copyGapCombination($orgID, $newID)
1043  {
1044  $assClozeGapCombinationObj = new assClozeGapCombination();
1045  $array = $assClozeGapCombinationObj->loadFromDb($orgID);
1046  $assClozeGapCombinationObj->importGapCombinationToDb($newID , $array);
1047  }
1048 
1055  {
1056  $output = $this->getClozeText();
1057  foreach ($this->getGaps() as $gap_index => $gap)
1058  {
1059  $answers = array();
1060  foreach ($gap->getItemsRaw() as $item)
1061  {
1062  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
1063  }
1064  $output = preg_replace("/\[gap\].*?\[\/gap\]/", "[_gap]" . $this->prepareTextareaOutput(join(",", $answers), true) . "[/_gap]", $output, 1);
1065  }
1066  $output = str_replace("_gap]", "gap]", $output);
1067  $this->cloze_text = $output;
1068  }
1069 
1079  function deleteAnswerText($gap_index, $answer_index)
1080  {
1081  if (array_key_exists($gap_index, $this->gaps))
1082  {
1083  if ($this->gaps[$gap_index]->getItemCount() == 1)
1084  {
1085  // this is the last answer text => remove the gap
1086  $this->deleteGap($gap_index);
1087  }
1088  else
1089  {
1090  // remove the answer text
1091  $this->gaps[$gap_index]->deleteItem($answer_index);
1092  $this->updateClozeTextFromGaps();
1093  }
1094  }
1095  }
1096 
1105  function deleteGap($gap_index)
1106  {
1107  if (array_key_exists($gap_index, $this->gaps))
1108  {
1109  $output = $this->getClozeText();
1110  foreach ($this->getGaps() as $replace_gap_index => $gap)
1111  {
1112  $answers = array();
1113  foreach ($gap->getItemsRaw() as $item)
1114  {
1115  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
1116  }
1117  if ($replace_gap_index == $gap_index)
1118  {
1119  $output = preg_replace("/\[gap\].*?\[\/gap\]/", "", $output, 1);
1120  }
1121  else
1122  {
1123  $output = preg_replace("/\[gap\].*?\[\/gap\]/", "[_gap]" . join(",", $answers) . "[/_gap]", $output, 1);
1124  }
1125  }
1126  $output = str_replace("_gap]", "gap]", $output);
1127  $this->cloze_text = $output;
1128  unset($this->gaps[$gap_index]);
1129  $this->gaps = array_values($this->gaps);
1130  }
1131  }
1132 
1142  function getTextgapPoints($a_original, $a_entered, $max_points)
1143  {
1144  include_once "./Services/Utilities/classes/class.ilStr.php";
1145  $result = 0;
1146  $gaprating = $this->getTextgapRating();
1147  switch ($gaprating)
1148  {
1150  if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) $result = $max_points;
1151  break;
1153  if (strcmp($a_original, $a_entered) == 0) $result = $max_points;
1154  break;
1156  if (levenshtein($a_original, $a_entered) <= 1) $result = $max_points;
1157  break;
1159  if (levenshtein($a_original, $a_entered) <= 2) $result = $max_points;
1160  break;
1162  if (levenshtein($a_original, $a_entered) <= 3) $result = $max_points;
1163  break;
1165  if (levenshtein($a_original, $a_entered) <= 4) $result = $max_points;
1166  break;
1168  if (levenshtein($a_original, $a_entered) <= 5) $result = $max_points;
1169  break;
1170  }
1171  return $result;
1172  }
1173 
1183  function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound)
1184  {
1185 // fau: fixGapFormula - check entered value by evalMath
1186 // if( ! $this->checkForValidFormula($a_entered) )
1187 // {
1188 // return 0;
1189 // }
1190 
1191  include_once "./Services/Math/classes/class.EvalMath.php";
1192  $eval = new EvalMath();
1193  $eval->suppress_errors = TRUE;
1194  $result = 0;
1195 
1196  if ($eval->e($a_entered) === FALSE)
1197  {
1198  return 0;
1199  }
1200  elseif (($eval->e($lowerBound) !== FALSE) && ($eval->e($upperBound) !== FALSE))
1201 // fau.
1202  {
1203  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) $result = $max_points;
1204  }
1205  else if ($eval->e($lowerBound) !== FALSE)
1206  {
1207  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) $result = $max_points;
1208  }
1209  else if ($eval->e($upperBound) !== FALSE)
1210  {
1211  if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) $result = $max_points;
1212  }
1213  else
1214  {
1215  if ($eval->e($a_entered) == $eval->e($a_original)) $result = $max_points;
1216  }
1217  return $result;
1218  }
1219 
1224  public function checkForValidFormula($value)
1225  {
1226  return preg_match("/^-?(\\d*)(,|\\.|\\/){0,1}(\\d*)$/", $value, $matches);
1227  }
1238  public function calculateReachedPoints($active_id, $pass = NULL, $returndetails = FALSE)
1239  {
1240  global $ilDB;
1241 
1242  if (is_null($pass))
1243  {
1244  $pass = $this->getSolutionMaxPass($active_id);
1245  }
1246 
1247  $result = $this->getCurrentSolutionResultSet($active_id, $pass);
1248  $user_result = array();
1249  while ($data = $ilDB->fetchAssoc($result))
1250  {
1251  if (strcmp($data["value2"], "") != 0)
1252  {
1253  $user_result[$data["value1"]] = array(
1254  "gap_id" => $data["value1"],
1255  "value" => $data["value2"]
1256  );
1257  }
1258  }
1259 
1260  ksort($user_result); // this is required when identical scoring for same solutions is disabled
1261 
1262  if ($returndetails)
1263  {
1264  $detailed = array();
1265  $this->calculateReachedPointsForSolution($user_result, $detailed);
1266  return $detailed;
1267  }
1268 
1269  return $this->calculateReachedPointsForSolution($user_result);
1270  }
1271 
1272  public function getSolutionSubmit()
1273  {
1274  $solutionSubmit = array();
1275 
1276  foreach ($_POST as $key => $value)
1277  {
1278  if (preg_match("/^gap_(\d+)/", $key, $matches))
1279  {
1280  $value = ilUtil::stripSlashes($value, FALSE);
1281  if (strlen($value))
1282  {
1283  $gap = $this->getGap($matches[1]);
1284  if (is_object($gap))
1285  {
1286  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1)))
1287  {
1288  if ($gap->getType() == CLOZE_NUMERIC)
1289  {
1290  $value = str_replace(",", ".", $value);
1291  }
1292  $solutionSubmit[trim($matches[1])] = $value;
1293  }
1294  }
1295  }
1296  }
1297  }
1298 
1299  return $solutionSubmit;
1300  }
1301 
1310  public function saveWorkingData($active_id, $pass = NULL)
1311  {
1312  global $ilDB;
1313  global $ilUser;
1314  if (is_null($pass))
1315  {
1316  include_once "./Modules/Test/classes/class.ilObjTest.php";
1317  $pass = ilObjTest::_getPass($active_id);
1318  }
1319 
1320  $this->getProcessLocker()->requestUserSolutionUpdateLock();
1321 
1322  $affectedRows = $this->removeCurrentSolution($active_id, $pass);
1323 
1324  $entered_values = 0;
1325 
1326  foreach($this->getSolutionSubmit() as $val1 => $val2)
1327  {
1328  $value = trim(ilUtil::stripSlashes($val2, FALSE));
1329  if (strlen($value))
1330  {
1331  $gap = $this->getGap(trim(ilUtil::stripSlashes($val1)));
1332  if (is_object($gap))
1333  {
1334  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1)))
1335  {
1336  $affectedRows = $this->saveCurrentSolution($active_id,$pass, $val1, $value);
1337  $entered_values++;
1338  }
1339  }
1340  }
1341  }
1342 
1343  $this->getProcessLocker()->releaseUserSolutionUpdateLock();
1344 
1345  if ($entered_values)
1346  {
1347  include_once ("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1349  {
1350  $this->logAction($this->lng->txtlng("assessment", "log_user_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
1351  }
1352  }
1353  else
1354  {
1355  include_once ("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1357  {
1358  $this->logAction($this->lng->txtlng("assessment", "log_user_not_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
1359  }
1360  }
1361 
1362  return TRUE;
1363  }
1364 
1373  protected function reworkWorkingData($active_id, $pass, $obligationsAnswered)
1374  {
1375  // nothing to rework!
1376  }
1377 
1384  function getQuestionType()
1385  {
1386  return "assClozeTest";
1387  }
1388 
1396  function getTextgapRating()
1397  {
1398  return $this->textgap_rating;
1399  }
1400 
1408  function setTextgapRating($a_textgap_rating)
1409  {
1410  switch ($a_textgap_rating)
1411  {
1419  $this->textgap_rating = $a_textgap_rating;
1420  break;
1421  default:
1422  $this->textgap_rating = TEXTGAP_RATING_CASEINSENSITIVE;
1423  break;
1424  }
1425  }
1426 
1435  {
1436  return ($this->identical_scoring) ? 1 : 0;
1437  }
1438 
1446  function setIdenticalScoring($a_identical_scoring)
1447  {
1448  $this->identical_scoring = ($a_identical_scoring) ? 1 : 0;
1449  }
1450 
1458  {
1459  return "qpl_qst_cloze";
1460  }
1461 
1469  {
1470  return array("qpl_a_cloze",'qpl_a_cloze_combi_res');
1471  }
1472 
1479  function setFixedTextLength($a_text_len)
1480  {
1481  $this->fixedTextLength = $a_text_len;
1482  }
1483 
1491  {
1492  return $this->fixedTextLength;
1493  }
1494 
1503  function getMaximumGapPoints($gap_index)
1504  {
1505  $points = 0;
1506  $gap_max_points = 0;
1507  if (array_key_exists($gap_index, $this->gaps))
1508  {
1509  $gap =& $this->gaps[$gap_index];
1510  foreach ($gap->getItems() as $answer)
1511  {
1512  if ($answer->getPoints() > $gap_max_points)
1513  {
1514  $gap_max_points = $answer->getPoints();
1515  }
1516  }
1517  $points += $gap_max_points;
1518  }
1519  return $points;
1520  }
1521 
1527  {
1529  }
1531  {
1533  }
1534 
1536  {
1537  return $this->gap_combinations;
1538  }
1539 
1540  function setGapCombinationsExists($value)
1541  {
1542  $this->gap_combinations_exists = $value;
1543  }
1544 
1545  function setGapCombinations($value)
1546  {
1547  $this->gap_combinations = $value;
1548  }
1561  public function setExportDetailsXLS(&$worksheet, $startrow, $active_id, $pass, &$format_title, &$format_bold)
1562  {
1563  include_once ("./Services/Excel/classes/class.ilExcelUtils.php");
1564  $solution = $this->getSolutionValues($active_id, $pass);
1565  $worksheet->writeString($startrow, 0, ilExcelUtils::_convert_text($this->lng->txt($this->getQuestionType())), $format_title);
1566  $worksheet->writeString($startrow, 1, ilExcelUtils::_convert_text($this->getTitle()), $format_title);
1567  $i = 1;
1568  foreach ($this->getGaps() as $gap_index => $gap)
1569  {
1570  $worksheet->writeString($startrow + $i, 0, ilExcelUtils::_convert_text($this->lng->txt("gap") . " $i"), $format_bold);
1571  $checked = FALSE;
1572  foreach ($solution as $solutionvalue)
1573  {
1574  if ($gap_index == $solutionvalue["value1"])
1575  {
1576  switch ($gap->getType())
1577  {
1578  case CLOZE_SELECT:
1579  $worksheet->writeString($startrow + $i, 1, $gap->getItem($solutionvalue["value2"])->getAnswertext());
1580  break;
1581  case CLOZE_NUMERIC:
1582  case CLOZE_TEXT:
1583  $worksheet->writeString($startrow + $i, 1, $solutionvalue["value2"]);
1584  break;
1585  }
1586  }
1587  }
1588  $i++;
1589  }
1590  return $startrow + $i + 1;
1591  }
1592 
1596  public function toJSON()
1597  {
1598  include_once("./Services/RTE/classes/class.ilRTE.php");
1599  $result = array();
1600  $result['id'] = (int) $this->getId();
1601  $result['type'] = (string) $this->getQuestionType();
1602  $result['title'] = (string) $this->getTitle();
1603  $result['question'] = $this->formatSAQuestion(
1604  $this->getQuestion() . '<br/>' . $this->getClozeText()
1605  );
1606  $result['nr_of_tries'] = (int) $this->getNrOfTries();
1607  $result['shuffle'] = (bool) $this->getShuffle();
1608  $result['feedback'] = array(
1609  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1610  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1611  );
1612 
1613  $gaps = array();
1614  foreach ($this->getGaps() as $key => $gap)
1615  {
1616  $items = array();
1617  foreach ($gap->getItems() as $item)
1618  {
1619  $jitem = array();
1620  $jitem['points'] = $item->getPoints();
1621  $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1622  $jitem['order'] = $item->getOrder();
1623  if ($gap->getType() == CLOZE_NUMERIC)
1624  {
1625  $jitem['lowerbound'] = $item->getLowerBound();
1626  $jitem['upperbound'] = $item->getUpperBound();
1627  }
1628  else
1629  {
1630  $jitem['value'] = trim($jitem['value']);
1631  }
1632  array_push($items, $jitem);
1633  }
1634 
1635  if( $gap->getGapSize() && ($gap->getType() == CLOZE_TEXT || $gap->getType() == CLOZE_NUMERIC) )
1636  {
1637  $jgap['size'] = $gap->getGapSize();
1638  }
1639 
1640  $jgap['shuffle'] = $gap->getShuffle();
1641  $jgap['type'] = $gap->getType();
1642  $jgap['item'] = $items;
1643 
1644  array_push($gaps, $jgap);
1645  }
1646  $result['gaps'] = $gaps;
1647  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1648  $result['mobs'] = $mobs;
1649  return json_encode($result);
1650  }
1651 
1660  public function getOperators($expression)
1661  {
1662  require_once "./Modules/TestQuestionPool/classes/class.ilOperatorsExpressionMapping.php";
1664  }
1665 
1670  public function getExpressionTypes()
1671  {
1672  return array(
1678  );
1679  }
1680 
1689  public function getUserQuestionResult($active_id, $pass)
1690  {
1692  global $ilDB;
1693  $result = new ilUserQuestionResult($this, $active_id, $pass);
1694 
1695  $data = $ilDB->queryF(
1696  "SELECT sol.value1+1 as val, sol.value2, cloze.cloze_type FROM tst_solutions sol INNER JOIN qpl_a_cloze cloze ON cloze.gap_id = value1 AND cloze.question_fi = sol.question_fi WHERE sol.active_fi = %s AND sol.pass = %s AND sol.question_fi = %s AND sol.step = (
1697  SELECT MAX(step) FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s
1698  ) GROUP BY sol.solution_id",
1699  array("integer", "integer", "integer","integer", "integer", "integer"),
1700  array($active_id, $pass, $this->getId(), $active_id, $pass, $this->getId())
1701  );
1702 
1703  while($row = $ilDB->fetchAssoc($data))
1704  {
1705  if($row["cloze_type"] == 1)
1706  {
1707  $row["value2"]++;
1708  }
1709  $result->addKeyValue($row["val"], $row["value2"]);
1710  }
1711 
1712  $points = $this->calculateReachedPoints($active_id, $pass);
1713  $max_points = $this->getMaximumPoints();
1714 
1715  $result->setReachedPercentage(($points/$max_points) * 100);
1716 
1717  return $result;
1718  }
1719 
1728  public function getAvailableAnswerOptions($index = null)
1729  {
1730  if($index !== null)
1731  {
1732  return $this->getGap($index);
1733  }
1734  else
1735  {
1736  return $this->getGaps();
1737  }
1738  }
1739 
1740  public function calculateCombinationResult($user_result)
1741  {
1742  $points = 0;
1743 
1744  $assClozeGapCombinationObj = new assClozeGapCombination();
1745 
1746  if($assClozeGapCombinationObj->combinationExistsForQid($this->getId()))
1747  {
1748  $combinations_for_question = $assClozeGapCombinationObj->getCleanCombinationArray($this->getId());
1749  $gap_answers = array();
1750  $gap_used_in_combination = array();
1751  foreach($user_result as $user_result_build_list)
1752  {
1753  if(is_array($user_result_build_list))
1754  {
1755  $gap_answers[$user_result_build_list['gap_id']] = $user_result_build_list['value'];
1756  }
1757  }
1758 
1759  foreach($combinations_for_question as $combination)
1760  {
1761 
1762  foreach($combination as $row_key => $row_answers)
1763  {
1764  $combination_fulfilled = true;
1765  $points_for_combination = $row_answers['points'];
1766  foreach($row_answers as $gap_key => $combination_gap_answer)
1767  {
1768  if($gap_key !== 'points')
1769  {
1770  $gap_used_in_combination[$gap_key]= $gap_key;
1771  }
1772  if($combination_fulfilled && array_key_exists($gap_key, $gap_answers))
1773  {
1774  switch($combination_gap_answer['type'])
1775  {
1776  case CLOZE_TEXT:
1777  $is_text_gap_correct = $this->getTextgapPoints($gap_answers[$gap_key], $combination_gap_answer['answer'], 1);
1778  if($is_text_gap_correct != 1)
1779  {
1780  $combination_fulfilled = false;
1781  }
1782  break;
1783  case CLOZE_SELECT:
1784  $answer = $this->gaps[$gap_key]->getItem($gap_answers[$gap_key]);
1785  $answertext = $answer->getAnswertext();
1786  if($answertext != $combination_gap_answer['answer'])
1787  {
1788  $combination_fulfilled = false;
1789  }
1790  break;
1791  case CLOZE_NUMERIC:
1792  $answer = $this->gaps[$gap_key]->getItem(0);
1793  if($combination_gap_answer['answer'] != 'out_of_bound')
1794  {
1795  $is_numeric_gap_correct = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1796  if($is_numeric_gap_correct != 1)
1797  {
1798  $combination_fulfilled = false;
1799  }
1800  }
1801  else
1802  {
1803  $wrong_is_the_new_right = $this->getNumericgapPoints($answer->getAnswertext(), $gap_answers[$gap_key], 1, $answer->getLowerBound(), $answer->getUpperBound());
1804  if($wrong_is_the_new_right == 1)
1805  {
1806  $combination_fulfilled = false;
1807  }
1808  }
1809  break;
1810  }
1811  }
1812  else
1813  {
1814  if($gap_key !== 'points')
1815  {
1816  $combination_fulfilled = false;
1817  }
1818  }
1819  }
1820  if($combination_fulfilled)
1821  {
1822  $points += $points_for_combination;
1823  }
1824  }
1825  }
1826  }
1827  return array($points, $gap_used_in_combination);
1828  }
1834  protected function calculateReachedPointsForSolution($user_result, &$detailed = null)
1835  {
1836  if($detailed === null)
1837  {
1838  $detailed = array();
1839  }
1840 
1841  $assClozeGapCombinationObj = new assClozeGapCombination();
1842  $combinations[1] = array();
1843  if($assClozeGapCombinationObj->combinationExistsForQid($this->getId()))
1844  {
1845  $combinations = $this->calculateCombinationResult($user_result);
1846  $points = $combinations[0];
1847  }
1848  $counter = 0;
1849  $solution_values_text = array(); // for identical scoring checks
1850  $solution_values_select = array(); // for identical scoring checks
1851  $solution_values_numeric = array(); // for identical scoring checks
1852  foreach($user_result as $gap_id => $value)
1853  {
1854  if(is_string($value))
1855  {
1856  $value = array("value" => $value);
1857  }
1858 
1859  if(array_key_exists($gap_id, $this->gaps) && !array_key_exists ($gap_id, $combinations[1]))
1860  {
1861  switch($this->gaps[$gap_id]->getType())
1862  {
1863  case CLOZE_TEXT:
1864  $gappoints = 0;
1865  for($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++)
1866  {
1867  $answer = $this->gaps[$gap_id]->getItem($order);
1868  $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1869  if($gotpoints > $gappoints) $gappoints = $gotpoints;
1870  }
1871  if(!$this->getIdenticalScoring())
1872  {
1873  // check if the same solution text was already entered
1874  if((in_array($value["value"], $solution_values_text)) && ($gappoints > 0))
1875  {
1876  $gappoints = 0;
1877  }
1878  }
1879  $points += $gappoints;
1880  $detailed[$gap_id] = array("points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? TRUE : FALSE, "positive" => ($gappoints > 0) ? TRUE : FALSE);
1881  array_push($solution_values_text, $value["value"]);
1882  break;
1883  case CLOZE_NUMERIC:
1884  $gappoints = 0;
1885  for($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++)
1886  {
1887  $answer = $this->gaps[$gap_id]->getItem($order);
1888  $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1889  if($gotpoints > $gappoints) $gappoints = $gotpoints;
1890  }
1891  if(!$this->getIdenticalScoring())
1892  {
1893  // check if the same solution value was already entered
1894  include_once "./Services/Math/classes/class.EvalMath.php";
1895  $eval = new EvalMath();
1896  $eval->suppress_errors = TRUE;
1897  $found_value = FALSE;
1898  foreach($solution_values_numeric as $solval)
1899  {
1900  if($eval->e($solval) == $eval->e($value["value"]))
1901  {
1902  $found_value = TRUE;
1903  }
1904  }
1905  if($found_value && ($gappoints > 0))
1906  {
1907  $gappoints = 0;
1908  }
1909  }
1910  $points += $gappoints;
1911  $detailed[$gap_id] = array("points" => $gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? TRUE : FALSE, "positive" => ($gappoints > 0) ? TRUE : FALSE);
1912  array_push($solution_values_numeric, $value["value"]);
1913  break;
1914  case CLOZE_SELECT:
1915  if($value["value"] >= 0)
1916  {
1917  for($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++)
1918  {
1919  $answer = $this->gaps[$gap_id]->getItem($order);
1920  if($value["value"] == $answer->getOrder())
1921  {
1922  $answerpoints = $answer->getPoints();
1923  if(!$this->getIdenticalScoring())
1924  {
1925  // check if the same solution value was already entered
1926  if((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0))
1927  {
1928  $answerpoints = 0;
1929  }
1930  }
1931  $points += $answerpoints;
1932  $detailed[$gap_id] = array("points" => $answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? TRUE : FALSE, "positive" => ($answerpoints > 0) ? TRUE : FALSE);
1933  array_push($solution_values_select, $answer->getAnswertext());
1934  }
1935  }
1936  }
1937  break;
1938  }
1939  }
1940  }
1941 
1942  return $points;
1943  }
1944 
1946  {
1947  $userSolution = array();
1948 
1949  foreach($previewSession->getParticipantsSolution() as $key => $val)
1950  {
1951  $userSolution[] = array('gap_id' => $key, 'value' => $val);
1952  }
1953 
1954  return $this->calculateReachedPointsForSolution($userSolution);
1955  }
1956 }