ILIAS  Release_4_4_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/interfaces/interface.ilObjQuestionScoringAdjustable.php';
7 require_once './Modules/TestQuestionPool/interfaces/interface.ilObjAnswerScoringAdjustable.php';
8 
21 {
29  var $gaps;
30 
39 
47  var $end_tag;
48 
60 
71 
78 
92  function __construct(
93  $title = "",
94  $comment = "",
95  $author = "",
96  $owner = -1,
97  $question = ""
98  )
99  {
101  $this->start_tag = "[gap]";
102  $this->end_tag = "[/gap]";
103  $this->gaps = array();
104  $this->setClozeText($question); // @TODO: Should this be $question?? See setter for why this is not trivial.
105  $this->fixedTextLength = "";
106  $this->identical_scoring = 1;
107  }
108 
114  public function isComplete()
115  {
116  if (strlen($this->getTitle())
117  && $this->getAuthor()
118  && $this->getClozeText()
119  && count($this->getGaps())
120  && $this->getMaximumPoints() > 0)
121  {
122  return true;
123  }
124  return false;
125  }
126 
134  public function cleanQuestiontext($text)
135  {
136  $text = preg_replace("/\[gap[^\]]*?\]/", "[gap]", $text);
137  $text = preg_replace("/<gap([^>]*?)>/", "[gap]", $text);
138  $text = str_replace("</gap>", "[/gap]", $text);
139  return $text;
140  }
141 
148  public function loadFromDb($question_id)
149  {
150  global $ilDB;
151  $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",
152  array("integer"),
153  array($question_id)
154  );
155  if ($result->numRows() == 1)
156  {
157  $data = $ilDB->fetchAssoc($result);
158  $this->setId($question_id);
159  $this->setNrOfTries($data['nr_of_tries']);
160  $this->setObjId($data["obj_fi"]);
161  $this->setTitle($data["title"]);
162  $this->setComment($data["description"]);
163  $this->setOriginalId($data["original_id"]);
164  $this->setAuthor($data["author"]);
165  $this->setPoints($data["points"]);
166  $this->setOwner($data["owner"]);
167  $this->setQuestion($this->cleanQuestiontext($data["question_text"]));
168  $this->setFixedTextLength($data["fixed_textlen"]);
169  $this->setIdenticalScoring(($data['tstamp'] == 0) ? true : $data["identical_scoring"]);
170  // replacement of old syntax with new syntax
171  include_once("./Services/RTE/classes/class.ilRTE.php");
172  $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1);
173  $this->setTextgapRating($data["textgap_rating"]);
174  $this->setEstimatedWorkingTime(substr($data["working_time"], 0, 2), substr($data["working_time"], 3, 2), substr($data["working_time"], 6, 2));
175 
176  try
177  {
178  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
179  }
181  {
182  }
183 
184  // open the cloze gaps with all answers
185  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
186  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
187  $result = $ilDB->queryF("SELECT * FROM qpl_a_cloze WHERE question_fi = %s ORDER BY gap_id, aorder ASC",
188  array("integer"),
189  array($question_id)
190  );
191  if ($result->numRows() > 0)
192  {
193  $this->gaps = array();
194  while ($data = $ilDB->fetchAssoc($result))
195  {
196  switch ($data["cloze_type"])
197  {
198  case CLOZE_TEXT:
199  if (!array_key_exists($data["gap_id"], $this->gaps))
200  {
201  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_TEXT);
202  }
203  $answer = new assAnswerCloze(
204  $data["answertext"],
205  $data["points"],
206  $data["aorder"]
207  );
208  $this->gaps[$data["gap_id"]]->addItem($answer);
209  break;
210  case CLOZE_SELECT:
211  if (!array_key_exists($data["gap_id"], $this->gaps))
212  {
213  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_SELECT);
214  $this->gaps[$data["gap_id"]]->setShuffle($data["shuffle"]);
215  }
216  $answer = new assAnswerCloze(
217  $data["answertext"],
218  $data["points"],
219  $data["aorder"]
220  );
221  $this->gaps[$data["gap_id"]]->addItem($answer);
222  break;
223  case CLOZE_NUMERIC:
224  if (!array_key_exists($data["gap_id"], $this->gaps))
225  {
226  $this->gaps[$data["gap_id"]] = new assClozeGap(CLOZE_NUMERIC);
227  }
228  $answer = new assAnswerCloze(
229  $data["answertext"],
230  $data["points"],
231  $data["aorder"]
232  );
233  $answer->setLowerBound($data["lowerlimit"]);
234  $answer->setUpperBound($data["upperlimit"]);
235  $this->gaps[$data["gap_id"]]->addItem($answer);
236  break;
237  }
238  }
239  }
240  }
241  parent::loadFromDb($question_id);
242  }
243 
244  #region Save question to db
245 
255  public function saveToDb($original_id = "")
256  {
260 
262  }
263 
267  public function saveAnswerSpecificDataToDb()
268  {
269  global $ilDB;
270 
271  $ilDB->manipulateF( "DELETE FROM qpl_a_cloze WHERE question_fi = %s",
272  array( "integer" ),
273  array( $this->getId() )
274  );
275 
276  foreach ($this->gaps as $key => $gap)
277  {
278  $this->saveClozeGapItemsToDb( $gap, $key );
279  }
280  }
281 
288  {
289  global $ilDB;
290 
291  $ilDB->manipulateF( "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
292  array( "integer" ),
293  array( $this->getId() )
294  );
295 
296  $ilDB->manipulateF( "INSERT INTO " . $this->getAdditionalTableName()
297  . " (question_fi, textgap_rating, identical_scoring, fixed_textlen) VALUES (%s, %s, %s, %s)",
298  array(
299  "integer",
300  "text",
301  "text",
302  "integer"
303  ),
304  array(
305  $this->getId(),
306  $this->getTextgapRating(),
307  $this->getIdenticalScoring(),
308  $this->getFixedTextLength() ? $this->getFixedTextLength() : NULL
309  )
310  );
311  }
312 
319  protected function saveClozeGapItemsToDb($gap, $key)
320  {
321  global $ilDB;
322  foreach ($gap->getItems() as $item)
323  {
324  $query = "";
325  $next_id = $ilDB->nextId( 'qpl_a_cloze' );
326  switch ($gap->getType())
327  {
328  case CLOZE_TEXT:
329  $this->saveClozeTextGapRecordToDb($next_id, $key, $item, $gap );
330  break;
331  case CLOZE_SELECT:
332  $this->saveClozeSelectGapRecordToDb($next_id, $key, $item, $gap );
333  break;
334  case CLOZE_NUMERIC:
335  $this->saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap );
336  break;
337  }
338  }
339  }
340 
349  protected function saveClozeTextGapRecordToDb($next_id, $key, $item, $gap)
350  {
351  global $ilDB;
352  $ilDB->manipulateF( "INSERT INTO qpl_a_cloze (answer_id, question_fi, gap_id, answertext, points, aorder, cloze_type) VALUES (%s, %s, %s, %s, %s, %s, %s)",
353  array(
354  "integer",
355  "integer",
356  "integer",
357  "text",
358  "float",
359  "integer",
360  "text"
361  ),
362  array(
363  $next_id,
364  $this->getId(),
365  $key,
366  strlen( $item->getAnswertext() ) ? $item->getAnswertext() : "",
367  $item->getPoints(),
368  $item->getOrder(),
369  $gap->getType()
370  )
371  );
372  }
373 
382  protected function saveClozeSelectGapRecordToDb($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, shuffle) 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  "text"
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->getShuffle()) ? "1" : "0"
405  )
406  );
407  }
408 
417  protected function saveClozeNumericGapRecordToDb($next_id, $key, $item, $gap)
418  {
419  global $ilDB;
420 
421  include_once "./Services/Math/classes/class.EvalMath.php";
422  $eval = new EvalMath();
423  $eval->suppress_errors = TRUE;
424 
425  // Bugfix for mantis: 14034
426  // It is a numeric gap, so cast to integer! // BH we should cast to float ;)
427  $answerText = strlen($item->getAnswertext()) ? (float)$item->getAnswertext() : 0;
428 
429  $lowerBound = (
430  $eval->e( $item->getLowerBound() !== FALSE ) && strlen( $item->getLowerBound() ) ?
431  $item->getLowerBound() : $item->getAnswertext()
432  );
433 
434  $upperBound = (
435  $eval->e( $item->getUpperBound() !== FALSE ) && strlen( $item->getUpperBound() ) ?
436  $item->getUpperBound() : $item->getAnswertext()
437  );
438 
439  //vd($answerText, $lowerBound, $upperBound);
440 
441  $ilDB->manipulateF( "INSERT INTO qpl_a_cloze (answer_id, question_fi, gap_id, answertext, points, aorder, cloze_type, lowerlimit, upperlimit) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
442  array(
443  "integer",
444  "integer",
445  "integer",
446  "text",
447  "float",
448  "integer",
449  "text",
450  "text",
451  "text"
452  ),
453  array(
454  $next_id,
455  $this->getId(),
456  $key,
457  $answerText,
458  $item->getPoints(),
459  $item->getOrder(),
460  $gap->getType(),
461  $lowerBound,
462  $upperBound
463  )
464  );
465  }
466 
467 
468 
469  #endregion Save question to db
470 
477  function getGaps()
478  {
479  return $this->gaps;
480  }
481 
482 
489  function flushGaps()
490  {
491  $this->gaps = array();
492  }
493 
503  function setClozeText($cloze_text = "")
504  {
505  $this->gaps = array();
506  $cloze_text = $this->cleanQuestiontext($cloze_text);
507  $this->question = $cloze_text;
509  }
510 
518  function getClozeText()
519  {
520  return $this->question;
521  }
522 
530  function getStartTag()
531  {
532  return $this->start_tag;
533  }
534 
542  function setStartTag($start_tag = "[gap]")
543  {
544  $this->start_tag = $start_tag;
545  }
546 
554  function getEndTag()
555  {
556  return $this->end_tag;
557  }
558 
566  function setEndTag($end_tag = "[/gap]")
567  {
568  $this->end_tag = $end_tag;
569  }
570 
578  {
579  include_once "./Modules/TestQuestionPool/classes/class.assClozeGap.php";
580  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
581  $search_pattern = "|\[gap\](.*?)\[/gap\]|i";
582  preg_match_all($search_pattern, $this->getClozeText(), $found);
583  $this->gaps = array();
584  if (count($found[0]))
585  {
586  foreach ($found[1] as $gap_index => $answers)
587  {
588  // create text gaps by default
589  $gap = new assClozeGap(CLOZE_TEXT);
590  $textparams = preg_split("/(?<!\\\\),/", $answers);
591  foreach ($textparams as $key => $value)
592  {
593  $answer = new assAnswerCloze($value, 0, $key);
594  $gap->addItem($answer);
595  }
596  $this->gaps[$gap_index] = $gap;
597  }
598  }
599  }
600 
606  function setGapType($gap_index, $gap_type)
607  {
608  if (array_key_exists($gap_index, $this->gaps))
609  {
610  $this->gaps[$gap_index]->setType($gap_type);
611  }
612  }
613 
623  function setGapShuffle($gap_index = 0, $shuffle = 1)
624  {
625  if (array_key_exists($gap_index, $this->gaps))
626  {
627  $this->gaps[$gap_index]->setShuffle($shuffle);
628  }
629  }
630 
637  function clearGapAnswers()
638  {
639  foreach ($this->gaps as $gap_index => $gap)
640  {
641  $this->gaps[$gap_index]->clearItems();
642  }
643  }
644 
652  function getGapCount()
653  {
654  if (is_array($this->gaps))
655  {
656  return count($this->gaps);
657  }
658  else
659  {
660  return 0;
661  }
662  }
663 
674  function addGapAnswer($gap_index, $order, $answer)
675  {
676  if (array_key_exists($gap_index, $this->gaps))
677  {
678  if ($this->gaps[$gap_index]->getType() == CLOZE_NUMERIC)
679  {
680  // only allow notation with "." for real numbers
681  $answer = str_replace(",", ".", $answer);
682  }
683  $this->gaps[$gap_index]->addItem(new assAnswerCloze($answer, 0, $order));
684  }
685  }
686 
695  function getGap($gap_index = 0)
696  {
697  if (array_key_exists($gap_index, $this->gaps))
698  {
699  return $this->gaps[$gap_index];
700  }
701  else
702  {
703  return NULL;
704  }
705  }
706 
717  function setGapAnswerPoints($gap_index, $order, $points)
718  {
719  if (array_key_exists($gap_index, $this->gaps))
720  {
721  $this->gaps[$gap_index]->setItemPoints($order, $points);
722  }
723  }
724 
733  function addGapText($gap_index)
734  {
735  if (array_key_exists($gap_index, $this->gaps))
736  {
737  include_once "./Modules/TestQuestionPool/classes/class.assAnswerCloze.php";
738  $answer = new assAnswerCloze(
739  "",
740  0,
741  $this->gaps[$gap_index]->getItemCount()
742  );
743  $this->gaps[$gap_index]->addItem($answer);
744  }
745  }
746 
755  function addGapAtIndex($gap, $index)
756  {
757  $this->gaps[$index] = $gap;
758  }
759 
770  function setGapAnswerLowerBound($gap_index, $order, $bound)
771  {
772  if (array_key_exists($gap_index, $this->gaps))
773  {
774  $this->gaps[$gap_index]->setItemLowerBound($order, $bound);
775  }
776  }
777 
788  function setGapAnswerUpperBound($gap_index, $order, $bound)
789  {
790  if (array_key_exists($gap_index, $this->gaps))
791  {
792  $this->gaps[$gap_index]->setItemUpperBound($order, $bound);
793  }
794  }
795 
802  function getMaximumPoints()
803  {
804  $points = 0;
805  foreach ($this->gaps as $gap_index => $gap)
806  {
807  if ($gap->getType() == CLOZE_TEXT)
808  {
809  $gap_max_points = 0;
810  foreach ($gap->getItems() as $item)
811  {
812  if ($item->getPoints() > $gap_max_points)
813  {
814  $gap_max_points = $item->getPoints();
815  }
816  }
817  $points += $gap_max_points;
818  }
819  else if ($gap->getType() == CLOZE_SELECT)
820  {
821  $srpoints = 0;
822  foreach ($gap->getItems() as $item)
823  {
824  if ($item->getPoints() > $srpoints)
825  {
826  $srpoints = $item->getPoints();
827  }
828  }
829  $points += $srpoints;
830  }
831  else if ($gap->getType() == CLOZE_NUMERIC)
832  {
833  $numpoints = 0;
834  foreach ($gap->getItems() as $item)
835  {
836  if ($item->getPoints() > $numpoints)
837  {
838  $numpoints = $item->getPoints();
839  }
840  }
841  $points += $numpoints;
842  }
843  }
844  return $points;
845  }
846 
852  function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null)
853  {
854  if ($this->id <= 0)
855  {
856  // The question has not been saved. It cannot be duplicated
857  return;
858  }
859  // duplicate the question in database
860  $this_id = $this->getId();
861  $thisObjId = $this->getObjId();
862 
863  $clone = $this;
864  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
866  $clone->id = -1;
867 
868  if( (int)$testObjId > 0 )
869  {
870  $clone->setObjId($testObjId);
871  }
872 
873  if ($title)
874  {
875  $clone->setTitle($title);
876  }
877  if ($author)
878  {
879  $clone->setAuthor($author);
880  }
881  if ($owner)
882  {
883  $clone->setOwner($owner);
884  }
885  if ($for_test)
886  {
887  $clone->saveToDb($original_id);
888  }
889  else
890  {
891  $clone->saveToDb();
892  }
893 
894  // copy question page content
895  $clone->copyPageOfQuestion($this_id);
896  // copy XHTML media objects
897  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
898 
899  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
900 
901  return $clone->getId();
902  }
903 
909  function copyObject($target_questionpool_id, $title = "")
910  {
911  if ($this->getId() <= 0)
912  {
913  // The question has not been saved. It cannot be duplicated
914  return;
915  }
916 
917  $thisId = $this->getId();
918  $thisObjId = $this->getObjId();
919 
920  $clone = $this;
921  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
923  $clone->id = -1;
924  $clone->setObjId($target_questionpool_id);
925  if ($title)
926  {
927  $clone->setTitle($title);
928  }
929  $clone->saveToDb();
930 
931  // copy question page content
932  $clone->copyPageOfQuestion($original_id);
933  // copy XHTML media objects
934  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
935 
936  $clone->onCopy($thisObjId, $thisId, $clone->getObjId(), $clone->getId());
937 
938  return $clone->getId();
939  }
940 
941  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "")
942  {
943  if ($this->id <= 0)
944  {
945  // The question has not been saved. It cannot be duplicated
946  return;
947  }
948 
949  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
950 
951  $sourceQuestionId = $this->id;
952  $sourceParentId = $this->getObjId();
953 
954  // duplicate the question in database
955  $clone = $this;
956  $clone->id = -1;
957 
958  $clone->setObjId($targetParentId);
959 
960  if ($targetQuestionTitle)
961  {
962  $clone->setTitle($targetQuestionTitle);
963  }
964 
965  $clone->saveToDb();
966  // copy question page content
967  $clone->copyPageOfQuestion($sourceQuestionId);
968  // copy XHTML media objects
969  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
970 
971  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
972 
973  return $clone->id;
974  }
975 
982  {
983  $output = $this->getClozeText();
984  foreach ($this->getGaps() as $gap_index => $gap)
985  {
986  $answers = array();
987  foreach ($gap->getItemsRaw() as $item)
988  {
989  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
990  }
991  $output = preg_replace("/\[gap\].*?\[\/gap\]/", "[_gap]" . $this->prepareTextareaOutput(join(",", $answers), true) . "[/_gap]", $output, 1);
992  }
993  $output = str_replace("_gap]", "gap]", $output);
994  $this->question = $output;
995  }
996 
1006  function deleteAnswerText($gap_index, $answer_index)
1007  {
1008  if (array_key_exists($gap_index, $this->gaps))
1009  {
1010  if ($this->gaps[$gap_index]->getItemCount() == 1)
1011  {
1012  // this is the last answer text => remove the gap
1013  $this->deleteGap($gap_index);
1014  }
1015  else
1016  {
1017  // remove the answer text
1018  $this->gaps[$gap_index]->deleteItem($answer_index);
1019  $this->updateClozeTextFromGaps();
1020  }
1021  }
1022  }
1023 
1032  function deleteGap($gap_index)
1033  {
1034  if (array_key_exists($gap_index, $this->gaps))
1035  {
1036  $output = $this->getClozeText();
1037  foreach ($this->getGaps() as $replace_gap_index => $gap)
1038  {
1039  $answers = array();
1040  foreach ($gap->getItemsRaw() as $item)
1041  {
1042  array_push($answers, str_replace(",", "\\,", $item->getAnswerText()));
1043  }
1044  if ($replace_gap_index == $gap_index)
1045  {
1046  $output = preg_replace("/\[gap\].*?\[\/gap\]/", "", $output, 1);
1047  }
1048  else
1049  {
1050  $output = preg_replace("/\[gap\].*?\[\/gap\]/", "[_gap]" . join(",", $answers) . "[/_gap]", $output, 1);
1051  }
1052  }
1053  $output = str_replace("_gap]", "gap]", $output);
1054  $this->question = $output;
1055  unset($this->gaps[$gap_index]);
1056  $this->gaps = array_values($this->gaps);
1057  }
1058  }
1059 
1069  function getTextgapPoints($a_original, $a_entered, $max_points)
1070  {
1071  include_once "./Services/Utilities/classes/class.ilStr.php";
1072  $result = 0;
1073  $gaprating = $this->getTextgapRating();
1074  switch ($gaprating)
1075  {
1077  if (strcmp(ilStr::strToLower($a_original), ilStr::strToLower($a_entered)) == 0) $result = $max_points;
1078  break;
1080  if (strcmp($a_original, $a_entered) == 0) $result = $max_points;
1081  break;
1083  if (levenshtein($a_original, $a_entered) <= 1) $result = $max_points;
1084  break;
1086  if (levenshtein($a_original, $a_entered) <= 2) $result = $max_points;
1087  break;
1089  if (levenshtein($a_original, $a_entered) <= 3) $result = $max_points;
1090  break;
1092  if (levenshtein($a_original, $a_entered) <= 4) $result = $max_points;
1093  break;
1095  if (levenshtein($a_original, $a_entered) <= 5) $result = $max_points;
1096  break;
1097  }
1098  return $result;
1099  }
1100 
1110  function getNumericgapPoints($a_original, $a_entered, $max_points, $lowerBound, $upperBound)
1111  {
1112  if( !is_numeric($a_entered) )
1113  {
1114  return 0;
1115  }
1116 
1117  include_once "./Services/Math/classes/class.EvalMath.php";
1118  $eval = new EvalMath();
1119  $eval->suppress_errors = TRUE;
1120  $result = 0;
1121  if (($eval->e($lowerBound) !== FALSE) && ($eval->e($upperBound) !== FALSE))
1122  {
1123  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($upperBound))) $result = $max_points;
1124  }
1125  else if ($eval->e($lowerBound) !== FALSE)
1126  {
1127  if (($eval->e($a_entered) >= $eval->e($lowerBound)) && ($eval->e($a_entered) <= $eval->e($a_original))) $result = $max_points;
1128  }
1129  else if ($eval->e($upperBound) !== FALSE)
1130  {
1131  if (($eval->e($a_entered) >= $eval->e($a_original)) && ($eval->e($a_entered) <= $eval->e($upperBound))) $result = $max_points;
1132  }
1133  else
1134  {
1135  if ($eval->e($a_entered) == $eval->e($a_original)) $result = $max_points;
1136  }
1137  return $result;
1138  }
1139 
1150  public function calculateReachedPoints($active_id, $pass = NULL, $returndetails = FALSE)
1151  {
1152  global $ilDB;
1153 
1154  $found_value1 = array();
1155  $found_value2 = array();
1156  $detailed = array();
1157  if (is_null($pass))
1158  {
1159  $pass = $this->getSolutionMaxPass($active_id);
1160  }
1161  $result = $ilDB->queryF("SELECT * FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1162  array(
1163  "integer",
1164  "integer",
1165  "integer"
1166  ),
1167  array(
1168  $active_id,
1169  $this->getId(),
1170  $pass
1171  )
1172  );
1173  $user_result = array();
1174  while ($data = $ilDB->fetchAssoc($result))
1175  {
1176  if (strcmp($data["value2"], "") != 0)
1177  {
1178  $user_result[$data["value1"]] = array(
1179  "gap_id" => $data["value1"],
1180  "value" => $data["value2"]
1181  );
1182  }
1183  }
1184 
1185  ksort($user_result); // this is required when identical scoring for same solutions is disabled
1186 
1187  $points = 0;
1188  $counter = 0;
1189  $solution_values_text = array(); // for identical scoring checks
1190  $solution_values_select = array(); // for identical scoring checks
1191  $solution_values_numeric = array(); // for identical scoring checks
1192  foreach ($user_result as $gap_id => $value)
1193  {
1194  if (array_key_exists($gap_id, $this->gaps))
1195  {
1196  switch ($this->gaps[$gap_id]->getType())
1197  {
1198  case CLOZE_TEXT:
1199  $gappoints = 0;
1200  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++)
1201  {
1202  $answer = $this->gaps[$gap_id]->getItem($order);
1203  $gotpoints = $this->getTextgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints());
1204  if ($gotpoints > $gappoints) $gappoints = $gotpoints;
1205  }
1206  if (!$this->getIdenticalScoring())
1207  {
1208  // check if the same solution text was already entered
1209  if ((in_array($value["value"], $solution_values_text)) && ($gappoints > 0))
1210  {
1211  $gappoints = 0;
1212  }
1213  }
1214  $points += $gappoints;
1215  $detailed[$gap_id] = array("points" =>$gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? TRUE : FALSE, "positive" => ($gappoints > 0) ? TRUE : FALSE);
1216  array_push($solution_values_text, $value["value"]);
1217  break;
1218  case CLOZE_NUMERIC:
1219  $gappoints = 0;
1220  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++)
1221  {
1222  $answer = $this->gaps[$gap_id]->getItem($order);
1223  $gotpoints = $this->getNumericgapPoints($answer->getAnswertext(), $value["value"], $answer->getPoints(), $answer->getLowerBound(), $answer->getUpperBound());
1224  if ($gotpoints > $gappoints) $gappoints = $gotpoints;
1225  }
1226  if (!$this->getIdenticalScoring())
1227  {
1228  // check if the same solution value was already entered
1229  include_once "./Services/Math/classes/class.EvalMath.php";
1230  $eval = new EvalMath();
1231  $eval->suppress_errors = TRUE;
1232  $found_value = FALSE;
1233  foreach ($solution_values_numeric as $solval)
1234  {
1235  if ($eval->e($solval) == $eval->e($value["value"]))
1236  {
1237  $found_value = TRUE;
1238  }
1239  }
1240  if ($found_value && ($gappoints > 0))
1241  {
1242  $gappoints = 0;
1243  }
1244  }
1245  $points += $gappoints;
1246  $detailed[$gap_id] = array("points" =>$gappoints, "best" => ($this->getMaximumGapPoints($gap_id) == $gappoints) ? TRUE : FALSE, "positive" => ($gappoints > 0) ? TRUE : FALSE);
1247  array_push($solution_values_numeric, $value["value"]);
1248  break;
1249  case CLOZE_SELECT:
1250  if ($value["value"] >= 0)
1251  {
1252  for ($order = 0; $order < $this->gaps[$gap_id]->getItemCount(); $order++)
1253  {
1254  $answer = $this->gaps[$gap_id]->getItem($order);
1255  if ($value["value"] == $answer->getOrder())
1256  {
1257  $answerpoints = $answer->getPoints();
1258  if (!$this->getIdenticalScoring())
1259  {
1260  // check if the same solution value was already entered
1261  if ((in_array($answer->getAnswertext(), $solution_values_select)) && ($answerpoints > 0))
1262  {
1263  $answerpoints = 0;
1264  }
1265  }
1266  $points += $answerpoints;
1267  $detailed[$gap_id] = array("points" =>$answerpoints, "best" => ($this->getMaximumGapPoints($gap_id) == $answerpoints) ? TRUE : FALSE, "positive" => ($answerpoints > 0) ? TRUE : FALSE);
1268  array_push($solution_values_select, $answer->getAnswertext());
1269  }
1270  }
1271  }
1272  break;
1273  }
1274  }
1275  }
1276  if ($returndetails)
1277  {
1278  return $detailed;
1279  }
1280  else
1281  {
1282  return $points;
1283  }
1284  }
1285 
1294  public function saveWorkingData($active_id, $pass = NULL)
1295  {
1296  global $ilDB;
1297  global $ilUser;
1298  if (is_null($pass))
1299  {
1300  include_once "./Modules/Test/classes/class.ilObjTest.php";
1301  $pass = ilObjTest::_getPass($active_id);
1302  }
1303 
1304  $this->getProcessLocker()->requestUserSolutionUpdateLock();
1305 
1306  $affectedRows = $ilDB->manipulateF("DELETE FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1307  array(
1308  "integer",
1309  "integer",
1310  "integer"
1311  ),
1312  array(
1313  $active_id,
1314  $this->getId(),
1315  $pass
1316  )
1317  );
1318 
1319  $entered_values = 0;
1320  foreach ($_POST as $key => $value)
1321  {
1322  if (preg_match("/^gap_(\d+)/", $key, $matches))
1323  {
1324  $value = ilUtil::stripSlashes($value, FALSE);
1325  if (strlen($value))
1326  {
1327  $gap = $this->getGap($matches[1]);
1328  if (is_object($gap))
1329  {
1330  if (!(($gap->getType() == CLOZE_SELECT) && ($value == -1)))
1331  {
1332  if ($gap->getType() == CLOZE_NUMERIC)
1333  {
1334  $value = str_replace(",", ".", $value);
1335  }
1336  $next_id = $ilDB->nextId("tst_solutions");
1337  $affectedRows = $ilDB->insert("tst_solutions", array(
1338  "solution_id" => array("integer", $next_id),
1339  "active_fi" => array("integer", $active_id),
1340  "question_fi" => array("integer", $this->getId()),
1341  "value1" => array("clob", trim($matches[1])),
1342  "value2" => array("clob", trim($value)),
1343  "pass" => array("integer", $pass),
1344  "tstamp" => array("integer", time())
1345  ));
1346  $entered_values++;
1347  }
1348  }
1349  }
1350  }
1351  }
1352 
1353  $this->getProcessLocker()->releaseUserSolutionUpdateLock();
1354 
1355  if ($entered_values)
1356  {
1357  include_once ("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1359  {
1360  $this->logAction($this->lng->txtlng("assessment", "log_user_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
1361  }
1362  }
1363  else
1364  {
1365  include_once ("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1367  {
1368  $this->logAction($this->lng->txtlng("assessment", "log_user_not_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
1369  }
1370  }
1371 
1372  return TRUE;
1373  }
1374 
1383  protected function reworkWorkingData($active_id, $pass, $obligationsAnswered)
1384  {
1385  // nothing to rework!
1386  }
1387 
1394  function getQuestionType()
1395  {
1396  return "assClozeTest";
1397  }
1398 
1406  function getTextgapRating()
1407  {
1408  return $this->textgap_rating;
1409  }
1410 
1418  function setTextgapRating($a_textgap_rating)
1419  {
1420  switch ($a_textgap_rating)
1421  {
1429  $this->textgap_rating = $a_textgap_rating;
1430  break;
1431  default:
1432  $this->textgap_rating = TEXTGAP_RATING_CASEINSENSITIVE;
1433  break;
1434  }
1435  }
1436 
1445  {
1446  return ($this->identical_scoring) ? 1 : 0;
1447  }
1448 
1456  function setIdenticalScoring($a_identical_scoring)
1457  {
1458  $this->identical_scoring = ($a_identical_scoring) ? 1 : 0;
1459  }
1460 
1468  {
1469  return "qpl_qst_cloze";
1470  }
1471 
1479  {
1480  return "qpl_a_cloze";
1481  }
1482 
1489  function setFixedTextLength($a_text_len)
1490  {
1491  $this->fixedTextLength = $a_text_len;
1492  }
1493 
1501  {
1502  return $this->fixedTextLength;
1503  }
1504 
1513  function getMaximumGapPoints($gap_index)
1514  {
1515  $points = 0;
1516  if (array_key_exists($gap_index, $this->gaps))
1517  {
1518  $gap =& $this->gaps[$gap_index];
1519 
1520  $gap_max_points = 0;
1521  foreach ($gap->getItems() as $answer)
1522  {
1523  if ($answer->getPoints() > $gap_max_points)
1524  {
1525  $gap_max_points = $answer->getPoints();
1526  }
1527  }
1528  $points += $gap_max_points;
1529  }
1530  return $points;
1531  }
1532 
1538  {
1540  }
1541 
1554  public function setExportDetailsXLS(&$worksheet, $startrow, $active_id, $pass, &$format_title, &$format_bold)
1555  {
1556  include_once ("./Services/Excel/classes/class.ilExcelUtils.php");
1557  $solution = $this->getSolutionValues($active_id, $pass);
1558  $worksheet->writeString($startrow, 0, ilExcelUtils::_convert_text($this->lng->txt($this->getQuestionType())), $format_title);
1559  $worksheet->writeString($startrow, 1, ilExcelUtils::_convert_text($this->getTitle()), $format_title);
1560  $i = 1;
1561  foreach ($this->getGaps() as $gap_index => $gap)
1562  {
1563  $worksheet->writeString($startrow + $i, 0, ilExcelUtils::_convert_text($this->lng->txt("gap") . " $i"), $format_bold);
1564  $checked = FALSE;
1565  foreach ($solution as $solutionvalue)
1566  {
1567  if ($gap_index == $solutionvalue["value1"])
1568  {
1569  switch ($gap->getType())
1570  {
1571  case CLOZE_SELECT:
1572  $worksheet->writeString($startrow + $i, 1, $gap->getItem($solutionvalue["value2"])->getAnswertext());
1573  break;
1574  case CLOZE_NUMERIC:
1575  case CLOZE_TEXT:
1576  $worksheet->writeString($startrow + $i, 1, $solutionvalue["value2"]);
1577  break;
1578  }
1579  }
1580  }
1581  $i++;
1582  }
1583  return $startrow + $i + 1;
1584  }
1585 
1589  public function toJSON()
1590  {
1591  include_once("./Services/RTE/classes/class.ilRTE.php");
1592  $result = array();
1593  $result['id'] = (int) $this->getId();
1594  $result['type'] = (string) $this->getQuestionType();
1595  $result['title'] = (string) $this->getTitle();
1596  $result['question'] = $this->formatSAQuestion($this->getQuestion());
1597  $result['nr_of_tries'] = (int) $this->getNrOfTries();
1598  $result['shuffle'] = (bool) $this->getShuffle();
1599  $result['feedback'] = array(
1600  "onenotcorrect" => $this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false),
1601  "allcorrect" => $this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true)
1602  );
1603  $gaps = array();
1604  foreach ($this->getGaps() as $key => $gap)
1605  {
1606  $items = array();
1607  foreach ($gap->getItems() as $item)
1608  {
1609  $jitem = array();
1610  $jitem['points'] = $item->getPoints();
1611  $jitem['value'] = $this->formatSAQuestion($item->getAnswertext());
1612  $jitem['order'] = $item->getOrder();
1613  if ($gap->getType() == CLOZE_NUMERIC)
1614  {
1615  $jitem['lowerbound'] = $item->getLowerBound();
1616  $jitem['upperbound'] = $item->getUpperBound();
1617  }
1618  else
1619  {
1620  $jitem['value'] = trim($jitem['value']);
1621  }
1622  array_push($items, $jitem);
1623  }
1624  $jgap['shuffle'] = $gap->getShuffle();
1625  $jgap['type'] = $gap->getType();
1626  $jgap['item'] = $items;
1627  array_push($gaps, $jgap);
1628 
1629  }
1630  $result['gaps'] = $gaps;
1631  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1632  $result['mobs'] = $mobs;
1633  return json_encode($result);
1634  }
1635 }