ILIAS  Release_5_0_x_branch Revision 61816
 All Data Structures Namespaces Files Functions Variables Groups Pages
class.assTextQuestion.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 
23 {
32 
41  var $keywords;
42 
43  var $answers;
44 
51 
52  /* method for automatic string matching */
53  private $matchcondition;
54 
55  public $keyword_relation = 'any';
56 
69  public function __construct(
70  $title = "",
71  $comment = "",
72  $author = "",
73  $owner = -1,
74  $question = ""
75  )
76  {
78  $this->maxNumOfChars = 0;
79  $this->points = 1;
80  $this->answers = array();
81  $this->matchcondition = 0;
82  }
83 
89  public function isComplete()
90  {
91  if (strlen($this->title)
92  && $this->author
93  && $this->question
94  && $this->getMaximumPoints() > 0
95  )
96  {
97  return true;
98  }
99  return false;
100  }
101 
107  public function saveToDb($original_id = "")
108  {
113  }
114 
122  function loadFromDb($question_id)
123  {
124  global $ilDB;
125 
126  $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",
127  array("integer"),
128  array($question_id)
129  );
130  if ($ilDB->numRows($result) == 1)
131  {
132  $data = $ilDB->fetchAssoc($result);
133  $this->setId($question_id);
134  $this->setObjId($data["obj_fi"]);
135  $this->setTitle($data["title"]);
136  $this->setComment($data["description"]);
137  $this->setOriginalId($data["original_id"]);
138  $this->setNrOfTries($data['nr_of_tries']);
139  $this->setAuthor($data["author"]);
140  $this->setPoints((float) $data["points"]);
141  $this->setOwner($data["owner"]);
142  include_once("./Services/RTE/classes/class.ilRTE.php");
143  $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data["question_text"], 1));
144  $this->setShuffle($data["shuffle"]);
145  $this->setMaxNumOfChars($data["maxnumofchars"]);
146  $this->setTextRating($this->isValidTextRating($data["textgap_rating"]) ? $data["textgap_rating"] : TEXTGAP_RATING_CASEINSENSITIVE);
147  $this->matchcondition = (strlen($data['matchcondition'])) ? $data['matchcondition'] : 0;
148  $this->setEstimatedWorkingTime(substr($data["working_time"], 0, 2), substr($data["working_time"], 3, 2), substr($data["working_time"], 6, 2));
149  $this->setKeywordRelation(($data['keyword_relation']));
150 
151  try
152  {
153  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
154  }
156  {
157  }
158  }
159 
160  $result = $ilDB->queryF("SELECT * FROM qpl_a_essay WHERE question_fi = %s",
161  array("integer"),
162  array($this->getId())
163  );
164 
165  $this->flushAnswers();
166  while ($row = $ilDB->fetchAssoc($result))
167  {
168  $this->addAnswer($row['answertext'], $row['points']);
169  }
170 
171  parent::loadFromDb($question_id);
172  }
173 
179  function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null)
180  {
181  if ($this->id <= 0)
182  {
183  // The question has not been saved. It cannot be duplicated
184  return;
185  }
186  // duplicate the question in database
187  $this_id = $this->getId();
188  $thisObjId = $this->getObjId();
189 
190  $clone = $this;
191  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
193  $clone->id = -1;
194 
195  if( (int)$testObjId > 0 )
196  {
197  $clone->setObjId($testObjId);
198  }
199 
200  if ($title)
201  {
202  $clone->setTitle($title);
203  }
204 
205  if ($author)
206  {
207  $clone->setAuthor($author);
208  }
209  if ($owner)
210  {
211  $clone->setOwner($owner);
212  }
213 
214  if ($for_test)
215  {
216  $clone->saveToDb($original_id);
217  }
218  else
219  {
220  $clone->saveToDb();
221  }
222 
223  // copy question page content
224  $clone->copyPageOfQuestion($this_id);
225  // copy XHTML media objects
226  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
227  #$clone->duplicateAnswers($this_id);
228 
229  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
230 
231  return $clone->id;
232  }
233 
239  function copyObject($target_questionpool_id, $title = "")
240  {
241  if ($this->id <= 0)
242  {
243  // The question has not been saved. It cannot be duplicated
244  return;
245  }
246  // duplicate the question in database
247  $clone = $this;
248  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
250  $clone->id = -1;
251  $source_questionpool_id = $this->getObjId();
252  $clone->setObjId($target_questionpool_id);
253  if ($title)
254  {
255  $clone->setTitle($title);
256  }
257  $clone->saveToDb();
258  // copy question page content
259  $clone->copyPageOfQuestion($original_id);
260  // copy XHTML media objects
261  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
262  // duplicate answers
263  #$clone->duplicateAnswers($original_id);
264 
265  $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
266 
267  return $clone->id;
268  }
269 
270  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "")
271  {
272  if ($this->id <= 0)
273  {
274  // The question has not been saved. It cannot be duplicated
275  return;
276  }
277 
278  include_once ("./Modules/TestQuestionPool/classes/class.assQuestion.php");
279 
280  $sourceQuestionId = $this->id;
281  $sourceParentId = $this->getObjId();
282 
283  // duplicate the question in database
284  $clone = $this;
285  $clone->id = -1;
286 
287  $clone->setObjId($targetParentId);
288 
289  if ($targetQuestionTitle)
290  {
291  $clone->setTitle($targetQuestionTitle);
292  }
293 
294  $clone->saveToDb();
295  // copy question page content
296  $clone->copyPageOfQuestion($sourceQuestionId);
297  // copy XHTML media objects
298  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
299  // duplicate answers
300  #$clone->duplicateAnswers($sourceQuestionId);
301 
302  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
303 
304  return $clone->id;
305  }
306 
314  function getMaxNumOfChars()
315  {
316  if (strcmp($this->maxNumOfChars, "") == 0)
317  {
318  return 0;
319  }
320  else
321  {
322  return $this->maxNumOfChars;
323  }
324  }
325 
333  function setMaxNumOfChars($maxchars = 0)
334  {
335  $this->maxNumOfChars = $maxchars;
336  }
337 
344  function getMaximumPoints()
345  {
346  if( in_array($this->getKeywordRelation(), self::getScoringModesWithPointsByQuestion()) )
347  {
348  return parent::getPoints();
349  }
350 
351  $points = 0;
352 
353  foreach ($this->answers as $answer)
354  {
355  if ($answer->getPoints() > 0)
356  {
357  $points = $points + $answer->getPoints();
358  }
359  }
360 
361  return $points;
362  }
363 
364  function getMinimumPoints()
365  {
366  if( in_array($this->getKeywordRelation(), self::getScoringModesWithPointsByQuestion()) )
367  {
368  return 0;
369  }
370 
371  $points = 0;
372 
373  foreach ($this->answers as $answer)
374  {
375  if ($answer->getPoints() < 0)
376  {
377  $points = $points + $answer->getPoints();
378  }
379  }
380 
381  return $points;
382  }
392  function setReachedPoints($active_id, $points, $pass = NULL)
393  {
394  global $ilDB;
395 
396  if (($points > 0) && ($points <= $this->getPoints()))
397  {
398  if (is_null($pass))
399  {
400  $pass = $this->getSolutionMaxPass($active_id);
401  }
402  $affectedRows = $ilDB->manipulateF("UPDATE tst_test_result SET points = %s WHERE active_fi = %s AND question_fi = %s AND pass = %s",
403  array('float','integer','integer','integer'),
404  array($points, $active_id, $this->getId(), $pass)
405  );
406  $this->_updateTestPassResults($active_id, $pass);
407  return TRUE;
408  }
409  else
410  {
411  return TRUE;
412  }
413  }
414 
415  private function isValidTextRating($textRating)
416  {
417  switch($textRating)
418  {
426  return true;
427  }
428 
429  return false;
430  }
431 
440  function isKeywordMatching($answertext, $a_keyword)
441  {
442  $result = FALSE;
443  $textrating = $this->getTextRating();
444  include_once "./Services/Utilities/classes/class.ilStr.php";
445  switch ($textrating)
446  {
448  if (ilStr::strPos(ilStr::strToLower($answertext), ilStr::strToLower($a_keyword)) !== false) return TRUE;
449  break;
451  if (ilStr::strPos($answertext, $a_keyword) !== false) return TRUE;
452  break;
453  }
454 
455  // "<p>red</p>" would not match "red" even with distance of 5
456  $answertext = strip_tags($answertext);
457 
458  $answerwords = array();
459  if (preg_match_all("/([^\s.]+)/", $answertext, $matches))
460  {
461  foreach ($matches[1] as $answerword)
462  {
463  array_push($answerwords, trim($answerword));
464  }
465  }
466  foreach ($answerwords as $a_original)
467  {
468  switch ($textrating)
469  {
471  if (levenshtein($a_original, $a_keyword) <= 1) return TRUE;
472  break;
474  if (levenshtein($a_original, $a_keyword) <= 2) return TRUE;
475  break;
477  if (levenshtein($a_original, $a_keyword) <= 3) return TRUE;
478  break;
480  if (levenshtein($a_original, $a_keyword) <= 4) return TRUE;
481  break;
483  if (levenshtein($a_original, $a_keyword) <= 5) return TRUE;
484  break;
485  }
486  }
487  return $result;
488  }
489 
490  protected function calculateReachedPointsForSolution($solution)
491  {
492  // Return min points when keyword relation is NON KEYWORDS
493  if( $this->getKeywordRelation() == 'non' )
494  {
495  return $this->getMinimumPoints();
496  }
497 
498  // Return min points if there are no answers present.
499  $answers = $this->getAnswers();
500 
501  if (count($answers) == 0)
502  {
503  return $this->getMinimumPoints();
504  }
505 
506  switch( $this->getKeywordRelation() )
507  {
508  case 'any':
509 
510  $points = 0;
511 
512  foreach ($answers as $answer)
513  {
514  $qst_answer = $answer->getAnswertext();
515  $user_answer = ' '.$solution;
516 
517  if( $this->isKeywordMatching( $user_answer, $qst_answer ) )
518  {
519  $points += $answer->getPoints();
520  }
521  }
522 
523  break;
524 
525  case 'all':
526 
527  $points = $this->getMaximumPoints();
528 
529  foreach ($answers as $answer)
530  {
531  $qst_answer = $answer->getAnswertext();
532  $user_answer = ' '.$solution;
533 
534  if( !$this->isKeywordMatching( $user_answer, $qst_answer ) )
535  {
536  $points = 0;
537  break;
538  }
539  }
540 
541  break;
542 
543  case 'one':
544 
545  $points = 0;
546 
547  foreach ($answers as $answer)
548  {
549  $qst_answer = $answer->getAnswertext();
550  $user_answer = ' '.$solution;
551 
552  if( $this->isKeywordMatching( $user_answer, $qst_answer ) )
553  {
554  $points = $this->getMaximumPoints();
555  break;
556  }
557  }
558 
559  break;
560  }
561 
562  return $points;
563  }
564 
575  public function calculateReachedPoints($active_id, $pass = NULL, $returndetails = FALSE)
576  {
577  if( $returndetails )
578  {
579  throw new ilTestException('return details not implemented for '.__METHOD__);
580  }
581 
582  global $ilDB;
583 
584  $points = 0;
585  if (is_null($pass))
586  {
587  $pass = $this->getSolutionMaxPass($active_id);
588  }
589 
590  $result = $ilDB->queryF("SELECT * FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
591  array('integer','integer','integer'),
592  array($active_id, $this->getId(), $pass)
593  );
594 
595  // Return min points when no answer was given.
596  if ($ilDB->numRows($result) == 0)
597  {
598  return $this->getMinimumPoints();
599  }
600 
601  // Return points of points are already on the row.
602  $row = $ilDB->fetchAssoc($result);
603  if ($row["points"] != NULL)
604  {
605  return $row["points"];
606  }
607 
608  return $this->calculateReachedPointsForSolution($row['value1']);
609  }
610 
619  public function saveWorkingData($active_id, $pass = NULL)
620  {
621  global $ilDB;
622  global $ilUser;
623 
624  include_once "./Services/Utilities/classes/class.ilStr.php";
625  if (is_null($pass))
626  {
627  include_once "./Modules/Test/classes/class.ilObjTest.php";
628  $pass = ilObjTest::_getPass($active_id);
629  }
630 
631  $this->getProcessLocker()->requestUserSolutionUpdateLock();
632 
633  $affectedRows = $ilDB->manipulateF("DELETE FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
634  array('integer','integer','integer'),
635  array($active_id, $this->getId(), $pass)
636  );
637 
638  $text = $this->getSolutionSubmit();
639 
640  $entered_values = 0;
641  if (strlen($text))
642  {
643  $next_id = $ilDB->nextId('tst_solutions');
644  $affectedRows = $ilDB->insert("tst_solutions", array(
645  "solution_id" => array("integer", $next_id),
646  "active_fi" => array("integer", $active_id),
647  "question_fi" => array("integer", $this->getId()),
648  "value1" => array("clob", trim($text)),
649  "value2" => array("clob", null),
650  "pass" => array("integer", $pass),
651  "tstamp" => array("integer", time())
652  ));
653  $entered_values++;
654  }
655 
656  $this->getProcessLocker()->releaseUserSolutionUpdateLock();
657 
658  if ($entered_values)
659  {
660  include_once ("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
662  {
663  $this->logAction($this->lng->txtlng("assessment", "log_user_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
664  }
665  }
666  else
667  {
668  include_once ("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
670  {
671  $this->logAction($this->lng->txtlng("assessment", "log_user_not_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
672  }
673  }
674 
675  return true;
676  }
677 
681  public function getSolutionSubmit()
682  {
683  $text = ilUtil::stripSlashes($_POST["TEXT"], FALSE);
684  if($this->getMaxNumOfChars())
685  {
686  include_once "./Services/Utilities/classes/class.ilStr.php";
687  $text_without_tags = preg_replace("/<[^>*?]>/is", "", $text);
688  $len_with_tags = ilStr::strLen($text);
689  $len_without_tags = ilStr::strLen($text_without_tags);
690  if($this->getMaxNumOfChars() < $len_without_tags)
691  {
692  if(!$this->isHTML($text))
693  {
694  $text = ilStr::subStr($text, 0, $this->getMaxNumOfChars());
695  }
696  }
697  }
698  if($this->isHTML($text))
699  {
700  $text = preg_replace("/<[^>]*$/ims", "", $text);
701  return $text;
702  } else
703  {
704  //$text = htmlentities($text, ENT_QUOTES, "UTF-8");
705  }
706  return $text;
707  }
708 
709  public function saveAdditionalQuestionDataToDb()
710  {
712  global $ilDB;
713  $ilDB->manipulateF( "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
714  array( "integer" ),
715  array( $this->getId()
716  )
717  );
718 
719  $ilDB->manipulateF( "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, maxnumofchars, keywords,
720  textgap_rating, matchcondition, keyword_relation) VALUES (%s, %s, %s, %s, %s, %s)",
721  array( "integer", "integer", "text", "text", 'integer', 'text' ),
722  array(
723  $this->getId(),
724  $this->getMaxNumOfChars(),
725  NULL,
726  $this->getTextRating(),
727  $this->matchcondition,
728  $this->getKeywordRelation()
729  )
730  );
731  }
732 
733  public function saveAnswerSpecificDataToDb()
734  {
736  global $ilDB;
737 
738  $ilDB->manipulateF( "DELETE FROM qpl_a_essay WHERE question_fi = %s",
739  array( "integer" ),
740  array( $this->getId() )
741  );
742 
743  foreach ($this->answers as $answer)
744  {
746  $nextID = $ilDB->nextId( 'qpl_a_essay' );
747  $ilDB->manipulateF( "INSERT INTO qpl_a_essay (answer_id, question_fi, answertext, points) VALUES (%s, %s, %s, %s)",
748  array( "integer", "integer", "text", 'float' ),
749  array(
750  $nextID,
751  $this->getId(),
752  $answer->getAnswertext(),
753  $answer->getPoints()
754  )
755  );
756  }
757  }
758 
767  protected function reworkWorkingData($active_id, $pass, $obligationsAnswered)
768  {
769  // nothing to rework!
770  }
771 
772  function createRandomSolution($test_id, $user_id)
773  {
774  }
775 
782  function getQuestionType()
783  {
784  return "assTextQuestion";
785  }
786 
794  function getTextRating()
795  {
796  return $this->text_rating;
797  }
798 
806  function setTextRating($a_text_rating)
807  {
808  switch ($a_text_rating)
809  {
817  $this->text_rating = $a_text_rating;
818  break;
819  default:
820  $this->text_rating = TEXTGAP_RATING_CASEINSENSITIVE;
821  break;
822  }
823  }
824 
832  {
833  return "qpl_qst_essay";
834  }
835 
841  {
843  }
844 
857  public function setExportDetailsXLS(&$worksheet, $startrow, $active_id, $pass, &$format_title, &$format_bold)
858  {
859  include_once ("./Services/Excel/classes/class.ilExcelUtils.php");
860  $solutions = $this->getSolutionValues($active_id, $pass);
861  $worksheet->writeString($startrow, 0, ilExcelUtils::_convert_text($this->lng->txt($this->getQuestionType())), $format_title);
862  $worksheet->writeString($startrow, 1, ilExcelUtils::_convert_text($this->getTitle()), $format_title);
863  $i = 1;
864  $worksheet->writeString($startrow + $i, 0, ilExcelUtils::_convert_text($this->lng->txt("result")), $format_bold);
865  if (strlen($solutions[0]["value1"]))
866  {
867  $worksheet->write($startrow + $i, 1, ilExcelUtils::_convert_text($solutions[0]["value1"]));
868  }
869  $i++;
870  return $startrow + $i + 1;
871  }
872 
876  public function toJSON()
877  {
878  include_once("./Services/RTE/classes/class.ilRTE.php");
879  $result = array();
880  $result['id'] = (int) $this->getId();
881  $result['type'] = (string) $this->getQuestionType();
882  $result['title'] = (string) $this->getTitle();
883  $result['question'] = $this->formatSAQuestion($this->getQuestion());
884  $result['nr_of_tries'] = (int) $this->getNrOfTries();
885  $result['shuffle'] = (bool) $this->getShuffle();
886  $result['maxlength'] = (int) $this->getMaxNumOfChars();
887  return json_encode($result);
888  }
889 
890  public function getAnswerCount()
891  {
892  return count($this->answers);
893  }
894 
908  public function addAnswer(
909  $answertext = "",
910  $points = 0.0,
911  $points_unchecked = 0.0,
912  $order = 0,
913  $answerimage = ""
914  )
915  {
916  include_once "./Modules/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php";
917 
918  // add answer
919  $answer = new ASS_AnswerMultipleResponseImage($answertext, $points);
920  $this->answers[] = $answer;
921  }
922 
923  public function getAnswers()
924  {
925  return $this->answers;
926  }
927 
937  function getAnswer($index = 0)
938  {
939  if ($index < 0) return NULL;
940  if (count($this->answers) < 1) return NULL;
941  if ($index >= count($this->answers)) return NULL;
942 
943  return $this->answers[$index];
944  }
945 
954  function deleteAnswer($index = 0)
955  {
956  if ($index < 0) return;
957  if (count($this->answers) < 1) return;
958  if ($index >= count($this->answers)) return;
959  $answer = $this->answers[$index];
960  if (strlen($answer->getImage())) $this->deleteImage($answer->getImage());
961  unset($this->answers[$index]);
962  $this->answers = array_values($this->answers);
963  for ($i = 0; $i < count($this->answers); $i++)
964  {
965  if ($this->answers[$i]->getOrder() > $index)
966  {
967  $this->answers[$i]->setOrder($i);
968  }
969  }
970  }
971 
973  {
974  return 'qpl_a_essay';
975  }
976 
983  function flushAnswers()
984  {
985  $this->answers = array();
986  }
987 
988  public function setAnswers($answers)
989  {
990  if( isset($answers['answer']) )
991  {
992  $count = count($answers['answer']);
993  $withPoints = true;
994  }
995  else
996  {
997  $count = count($answers);
998  $withPoints = false;
999  }
1000 
1001  $this->flushAnswers();
1002 
1003  for( $i = 0; $i < $count; $i++ )
1004  {
1005  if($withPoints)
1006  {
1007  $this->addAnswer($answers['answer'][$i], $answers['points'][$i]);
1008  }
1009  else
1010  {
1011  $this->addAnswer($answers[$i], 0);
1012  }
1013  }
1014  }
1015 
1017  {
1018  global $ilDB;
1019 
1020  $result = $ilDB->queryF("SELECT * FROM qpl_a_essay WHERE question_fi = %s",
1021  array('integer'),
1022  array($original_id)
1023  );
1024  if ($result->numRows())
1025  {
1026  while ($row = $ilDB->fetchAssoc($result))
1027  {
1028  $next_id = $ilDB->nextId('qpl_a_essay');
1029  $affectedRows = $ilDB->manipulateF(
1030  "INSERT INTO qpl_a_essay (answer_id, question_fi, answertext, points)
1031  VALUES (%s, %s, %s, %s)",
1032  array('integer','integer','text','float'),
1033  array($next_id, $this->getId(), $row["answertext"], $row["points"])
1034  );
1035  }
1036  }
1037  }
1038 
1039  public function getKeywordRelation()
1040  {
1041  return $this->keyword_relation;
1042  }
1043 
1048  public function setKeywordRelation($a_relation)
1049  {
1050  $this->keyword_relation = $a_relation;
1051  }
1052 
1053  public static function getValidScoringModes()
1054  {
1055  return array_merge(self::getScoringModesWithPointsByQuestion(), self::getScoringModesWithPointsByKeyword());
1056  }
1057 
1058  public static function getScoringModesWithPointsByQuestion()
1059  {
1060  return array('non', 'all', 'one');
1061  }
1062 
1063  public static function getScoringModesWithPointsByKeyword()
1064  {
1065  return array('any');
1066  }
1067 
1068 
1079  public function isAnswered($active_id, $pass)
1080  {
1081  $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId());
1082 
1083  return $numExistingSolutionRecords > 0;
1084  }
1085 
1096  public static function isObligationPossible($questionId)
1097  {
1098  return true;
1099  }
1100 }