ILIAS  release_7 Revision v7.30-3-g800a261c036
All Data Structures Namespaces Files Functions Variables Modules Pages
class.assQuestion.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 include_once "./Modules/Test/classes/inc.AssessmentConstants.php";
5 
20 abstract class assQuestion
21 {
22  const IMG_MIME_TYPE_JPG = 'image/jpeg';
23  const IMG_MIME_TYPE_PNG = 'image/png';
24  const IMG_MIME_TYPE_GIF = 'image/gif';
25 
26  protected static $allowedFileExtensionsByMimeType = array(
27  self::IMG_MIME_TYPE_JPG => array('jpg', 'jpeg'),
28  self::IMG_MIME_TYPE_PNG => array('png'),
29  self::IMG_MIME_TYPE_GIF => array('gif')
30  );
31 
32  protected static $allowedCharsetsByMimeType = array(
33  self::IMG_MIME_TYPE_JPG => array('binary'),
34  self::IMG_MIME_TYPE_PNG => array('binary'),
35  self::IMG_MIME_TYPE_GIF => array('binary')
36  );
37 
43  protected $id;
44 
50  protected $title;
51 
57  protected $comment;
58 
64  protected $owner;
65 
71  protected $author;
72 
78  protected $question;
79 
85  protected $points;
86 
92  protected $est_working_time;
93 
99  protected $shuffle;
100 
106  protected $test_id;
107 
113  protected $obj_id;
114 
120  protected $ilias;
121 
125  protected $tpl;
126 
130  protected $lng;
131 
135  protected $db;
136 
140  protected $refinery;
141 
148 
155 
161  protected $original_id;
162 
168  protected $page;
169 
173  private $nr_of_tries;
174 
178  private $arrData;
179 
184 
189  protected $external_id = '';
190 
195 
200 
207 
213  public $feedbackOBJ = null;
214 
220  public $prevent_rte_usage = false;
221 
228 
234  public $defaultnroftries = 0;
235 
239  protected $questionChangeListeners = array();
240 
244  protected $processLocker;
245 
246  public $questionActionCmd = 'handleQuestionAction';
247 
251  private static $resultGateway = null;
252 
256  protected $step = null;
257 
258  protected $lastChange;
259 
263  protected $shuffler;
264 
268  private $obligationsToBeConsidered = false;
269 
270  // fau: testNav - new variable $testQuestionConfig
275  // fau.
276 
280  protected $lifecycle;
281 
283  'image/jpeg' => array('jpg', 'jpeg'), 'image/png' => array('png'), 'image/gif' => array('gif')
284  );
285 
296  public function __construct(
297  $title = "",
298  $comment = "",
299  $author = "",
300  $owner = -1,
301  $question = ""
302  ) {
304  global $DIC;
305  $ilias = $DIC['ilias'];
306  $lng = $DIC['lng'];
307  $tpl = $DIC['tpl'];
308  $ilDB = $DIC['ilDB'];
309  $refinery = $DIC['refinery'];
310 
311  $this->ilias = $ilias;
312  $this->lng = $lng;
313  $this->tpl = $tpl;
314  $this->db = $ilDB;
315  $this->refinery = $refinery;
316 
317  $this->original_id = null;
318  $this->title = $title;
319  $this->comment = $comment;
320  $this->page = null;
321  $this->author = $author;
322  $this->setQuestion($question);
323  if (!$this->author) {
324  $this->author = $this->ilias->account->fullname;
325  }
326  $this->owner = $owner;
327  if ($this->owner <= 0) {
328  $this->owner = $this->ilias->account->id;
329  }
330  $this->id = -1;
331  $this->test_id = -1;
332  $this->suggested_solutions = array();
333  $this->shuffle = 1;
334  $this->nr_of_tries = 0;
335  $this->setEstimatedWorkingTime(0, 1, 0);
336  $this->arrData = array();
337  $this->setExternalId('');
338 
339  $this->questionActionCmd = 'handleQuestionAction';
340 
341  $this->lastChange = null;
342 
343  require_once 'Services/Randomization/classes/class.ilArrayElementOrderKeeper.php';
344  $this->shuffler = new ilArrayElementOrderKeeper();
345 
346  $this->lifecycle = ilAssQuestionLifecycle::getDraftInstance();
347  }
348 
349  protected static $forcePassResultsUpdateEnabled = false;
350 
352  {
353  self::$forcePassResultsUpdateEnabled = $forcePassResultsUpdateEnabled;
354  }
355 
356  public static function isForcePassResultUpdateEnabled()
357  {
358  return self::$forcePassResultsUpdateEnabled;
359  }
360 
361  public static function isAllowedImageMimeType($mimeType)
362  {
363  return (bool) count(self::getAllowedFileExtensionsForMimeType($mimeType));
364  }
365 
366  public static function fetchMimeTypeIdentifier($contentTypeString)
367  {
368  return current(explode(';', $contentTypeString));
369  }
370 
371  public static function getAllowedFileExtensionsForMimeType($mimeType)
372  {
373  foreach (self::$allowedFileExtensionsByMimeType as $allowedMimeType => $extensions) {
374  $rexCharsets = implode('|', self::$allowedCharsetsByMimeType[$allowedMimeType]);
375  $rexMimeType = preg_quote($allowedMimeType, '/');
376 
377  $rex = '/^' . $rexMimeType . '(;(\s)*charset=(' . $rexCharsets . '))*$/';
378 
379  if (!preg_match($rex, $mimeType)) {
380  continue;
381  }
382 
383  return $extensions;
384  }
385 
386  return array();
387  }
388 
389  public static function isAllowedImageFileExtension($mimeType, $fileExtension)
390  {
391  return in_array(
392  strtolower($fileExtension),
393  self::getAllowedFileExtensionsForMimeType($mimeType)
394  );
395  }
396 
397  // hey: prevPassSolutions - question action actracted (heavy use in fileupload refactoring)
398 
402  protected function getQuestionAction()
403  {
404  if (!isset($_POST['cmd']) || !isset($_POST['cmd'][$this->questionActionCmd])) {
405  return '';
406  }
407 
408  if (!is_array($_POST['cmd'][$this->questionActionCmd]) || !count($_POST['cmd'][$this->questionActionCmd])) {
409  return '';
410  }
411 
412  return key($_POST['cmd'][$this->questionActionCmd]);
413  }
414 
419  protected function isNonEmptyItemListPostSubmission($postSubmissionFieldname)
420  {
421  if (!isset($_POST[$postSubmissionFieldname])) {
422  return false;
423  }
424 
425  if (!is_array($_POST[$postSubmissionFieldname])) {
426  return false;
427  }
428 
429  if (!count($_POST[$postSubmissionFieldname])) {
430  return false;
431  }
432 
433  return true;
434  }
435 
441  protected function ensureCurrentTestPass($active_id, $pass)
442  {
443  if (is_integer($pass) && $pass >= 0) {
444  return $pass;
445  }
446 
447  return $this->lookupCurrentTestPass($active_id, $pass);
448  }
449 
455  protected function lookupCurrentTestPass($active_id, $pass)
456  {
457  require_once 'Modules/Test/classes/class.ilObjTest.php';
458  return ilObjTest::_getPass($active_id);
459  }
460 
465  protected function lookupTestId($active_id)
466  {
467  global $DIC; /* @var ILIAS\DI\Container $DIC */
468  $ilDB = $DIC['ilDB'];
469 
470  $result = $ilDB->queryF(
471  "SELECT test_fi FROM tst_active WHERE active_id = %s",
472  array('integer'),
473  array($active_id)
474  );
475 
476  while ($row = $ilDB->fetchAssoc($result)) {
477  return $row["test_fi"];
478  }
479 
480  return null;
481  }
482  // hey.
483 
488  protected function log($active_id, $langVar)
489  {
491  $message = $this->lng->txtlng('assessment', $langVar, ilObjAssessmentFolder::_getLogLanguage());
492  assQuestion::logAction($message, $active_id, $this->getId());
493  }
494  }
495 
499  public static function getAllowedImageMaterialFileExtensions()
500  {
501  $extensions = array();
502 
503  foreach (self::$allowedImageMaterialFileExtensionsByMimeType as $mimeType => $mimeExtensions) {
504  $extensions = array_merge($extensions, $mimeExtensions);
505  }
506  return array_unique($extensions);
507  }
508 
512  public function getShuffler()
513  {
514  return $this->shuffler;
515  }
516 
521  {
522  $this->shuffler = $shuffler;
523  }
524 
529  {
530  $this->processLocker = $processLocker;
531  }
532 
536  public function getProcessLocker()
537  {
538  return $this->processLocker;
539  }
540 
552  public function fromXML(
553  &$item,
554  &$questionpool_id,
555  &$tst_id,
556  &$tst_object,
557  &$question_counter,
558  &$import_mapping,
559  array $solutionhints = []
560  ) {
561  include_once "./Modules/TestQuestionPool/classes/import/qti12/class." . $this->getQuestionType() . "Import.php";
562  $classname = $this->getQuestionType() . "Import";
563  $import = new $classname($this);
564  $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping);
565 
566  foreach ($solutionhints as $hint) {
567  $h = new ilAssQuestionHint();
568  $h->setQuestionId($import->getQuestionId());
569  $h->setIndex($hint['index']);
570  $h->setPoints($hint['points']);
571  $h->setText($hint['txt']);
572  $h->save();
573  }
574  }
575 
582  public function toXML($a_include_header = true, $a_include_binary = true, $a_shuffle = false, $test_output = false, $force_image_references = false)
583  {
584  include_once "./Modules/TestQuestionPool/classes/export/qti12/class." . $this->getQuestionType() . "Export.php";
585  $classname = $this->getQuestionType() . "Export";
586  $export = new $classname($this);
587  return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
588  }
589 
596  public function isComplete()
597  {
598  return false;
599  }
600 
608  public function questionTitleExists($questionpool_id, $title)
609  {
610  global $DIC;
611  $ilDB = $DIC['ilDB'];
612 
613  $result = $ilDB->queryF(
614  "SELECT * FROM qpl_questions WHERE obj_fi = %s AND title = %s",
615  array('integer','text'),
616  array($questionpool_id, $title)
617  );
618  return ($result->numRows() > 0) ? true : false;
619  }
620 
628  public function setTitle($title = "")
629  {
630  $this->title = $title;
631  }
632 
640  public function setId($id = -1)
641  {
642  $this->id = $id;
643  }
644 
652  public function setTestId($id = -1)
653  {
654  $this->test_id = $id;
655  }
656 
664  public function setComment($comment = "")
665  {
666  $this->comment = $comment;
667  }
668 
677  {
678  $this->outputType = $outputType;
679  }
680 
681 
689  public function setShuffle($shuffle = true)
690  {
691  if ($shuffle) {
692  $this->shuffle = 1;
693  } else {
694  $this->shuffle = 0;
695  }
696  }
697 
708  public function setEstimatedWorkingTime($hour = 0, $min = 0, $sec = 0)
709  {
710  $this->est_working_time = array("h" => (int) $hour, "m" => (int) $min, "s" => (int) $sec);
711  }
712 
719  public function setEstimatedWorkingTimeFromDurationString($durationString)
720  {
721  $this->est_working_time = array(
722  'h' => (int) substr($durationString, 0, 2),
723  'm' => (int) substr($durationString, 3, 2),
724  's' => (int) substr($durationString, 6, 2)
725  );
726  }
727 
735  public function keyInArray($searchkey, $array)
736  {
737  if ($searchkey) {
738  foreach ($array as $key => $value) {
739  if (strcmp($key, $searchkey) == 0) {
740  return true;
741  }
742  }
743  }
744  return false;
745  }
746 
754  public function setAuthor($author = "")
755  {
756  if (!$author) {
757  $author = $this->ilias->account->fullname;
758  }
759  $this->author = $author;
760  }
761 
769  public function setOwner($owner = "")
770  {
771  $this->owner = $owner;
772  }
773 
781  public function getTitle()
782  {
783  return $this->title;
784  }
785 
791  public function getTitleFilenameCompliant()
792  {
793  require_once 'Services/Utilities/classes/class.ilUtil.php';
794  return ilUtil::getASCIIFilename($this->getTitle());
795  }
796 
804  public function getId()
805  {
806  return $this->id;
807  }
808 
816  public function getShuffle()
817  {
818  return $this->shuffle;
819  }
820 
828  public function getTestId()
829  {
830  return $this->test_id;
831  }
832 
840  public function getComment()
841  {
842  return $this->comment;
843  }
844 
852  public function getOutputType()
853  {
854  return $this->outputType;
855  }
856 
857  public function getDescriptionForHTMLOutput() : string
858  {
859  return $this->refinery->string()->stripTags()->transform($this->comment ?? '');
860  }
861 
868  public function supportsJavascriptOutput()
869  {
870  return false;
871  }
872 
873  public function supportsNonJsOutput()
874  {
875  return true;
876  }
877 
878  public function requiresJsSwitch()
879  {
880  return $this->supportsJavascriptOutput() && $this->supportsNonJsOutput();
881  }
882 
890  public function getEstimatedWorkingTime()
891  {
892  if (!$this->est_working_time) {
893  $this->est_working_time = array("h" => 0, "m" => 0, "s" => 0);
894  }
896  }
897 
905  public function getAuthor()
906  {
907  return $this->author;
908  }
909 
910  public function getAuthorForHTMLOutput() : string
911  {
912  return $this->refinery->string()->stripTags()->transform($this->author);
913  }
914 
922  public function getOwner()
923  {
924  return $this->owner;
925  }
926 
934  public function getObjId()
935  {
936  return $this->obj_id;
937  }
938 
946  public function setObjId($obj_id = 0)
947  {
948  $this->obj_id = $obj_id;
949  }
950 
954  public function getLifecycle()
955  {
956  return $this->lifecycle;
957  }
958 
963  {
964  $this->lifecycle = $lifecycle;
965  }
966 
970  public function setExternalId($external_id)
971  {
972  $this->external_id = $external_id;
973  }
974 
978  public function getExternalId()
979  {
980  if (!strlen($this->external_id)) {
981  if ($this->getId() > 0) {
982  return 'il_' . IL_INST_ID . '_qst_' . $this->getId();
983  } else {
984  return uniqid('', true);
985  }
986  } else {
987  return $this->external_id;
988  }
989  }
990 
997  public static function _getMaximumPoints($question_id)
998  {
999  global $DIC;
1000  $ilDB = $DIC['ilDB'];
1001 
1002  $points = 0;
1003  $result = $ilDB->queryF(
1004  "SELECT points FROM qpl_questions WHERE question_id = %s",
1005  array('integer'),
1006  array($question_id)
1007  );
1008  if ($result->numRows() == 1) {
1009  $row = $ilDB->fetchAssoc($result);
1010  $points = $row["points"];
1011  }
1012  return $points;
1013  }
1014 
1021  public static function _getQuestionInfo($question_id)
1022  {
1023  global $DIC;
1024  $ilDB = $DIC['ilDB'];
1025 
1026  $result = $ilDB->queryF(
1027  "SELECT qpl_questions.*, qpl_qst_type.type_tag FROM qpl_qst_type, qpl_questions WHERE qpl_questions.question_id = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id",
1028  array('integer'),
1029  array($question_id)
1030  );
1031  if ($result->numRows()) {
1032  return $ilDB->fetchAssoc($result);
1033  } else {
1034  return array();
1035  }
1036  }
1037 
1044  public static function _getSuggestedSolutionCount($question_id)
1045  {
1046  global $DIC;
1047  $ilDB = $DIC['ilDB'];
1048 
1049  $result = $ilDB->queryF(
1050  "SELECT suggested_solution_id FROM qpl_sol_sug WHERE question_fi = %s",
1051  array('integer'),
1052  array($question_id)
1053  );
1054  return $result->numRows();
1055  }
1056 
1063  public static function _getSuggestedSolutionOutput($question_id)
1064  {
1066  if (!is_object($question)) {
1067  return "";
1068  }
1069  return $question->getSuggestedSolutionOutput();
1070  }
1071 
1072  public function getSuggestedSolutionOutput()
1073  {
1074  $output = array();
1075  foreach ($this->suggested_solutions as $solution) {
1076  switch ($solution["type"]) {
1077  case "lm":
1078  case "st":
1079  case "pg":
1080  case "git":
1081  array_push($output, '<a href="' . assQuestion::_getInternalLinkHref($solution["internal_link"]) . '">' . $this->lng->txt("solution_hint") . '</a>');
1082  break;
1083  case "file":
1084  $possible_texts = array_values(array_filter(array(
1085  ilUtil::prepareFormOutput($solution['value']['filename']),
1086  ilUtil::prepareFormOutput($solution['value']['name']),
1087  $this->lng->txt('tst_show_solution_suggested')
1088  )));
1089 
1090  require_once 'Services/WebAccessChecker/classes/class.ilWACSignedPath.php';
1092  array_push($output, '<a href="' . ilWACSignedPath::signFile($this->getSuggestedSolutionPathWeb() . $solution["value"]["name"]) . '">' . $possible_texts[0] . '</a>');
1093  break;
1094  case "text":
1095  $solutionValue = $solution["value"];
1096  $solutionValue = $this->fixSvgToPng($solutionValue);
1097  $solutionValue = $this->fixUnavailableSkinImageSources($solutionValue);
1098  $output[] = $this->prepareTextareaOutput($solutionValue, true);
1099  break;
1100  }
1101  }
1102  return join("<br />", $output);
1103  }
1104 
1113  public function &_getSuggestedSolution($question_id, $subquestion_index = 0)
1114  {
1115  global $DIC;
1116  $ilDB = $DIC['ilDB'];
1117 
1118  $result = $ilDB->queryF(
1119  "SELECT * FROM qpl_sol_sug WHERE question_fi = %s AND subquestion_index = %s",
1120  array('integer','integer'),
1121  array($question_id, $subquestion_index)
1122  );
1123  if ($result->numRows() == 1) {
1124  $row = $ilDB->fetchAssoc($result);
1125  return array(
1126  "internal_link" => $row["internal_link"],
1127  "import_id" => $row["import_id"]
1128  );
1129  } else {
1130  return array();
1131  }
1132  }
1133 
1139  public function getSuggestedSolutions()
1140  {
1142  }
1143 
1151  public static function _getReachedPoints($active_id, $question_id, $pass = null)
1152  {
1153  global $DIC;
1154  $ilDB = $DIC['ilDB'];
1155 
1156  $points = 0;
1157  if (is_null($pass)) {
1158  include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
1159  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
1160  }
1161  $result = $ilDB->queryF(
1162  "SELECT * FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1163  array('integer','integer','integer'),
1164  array($active_id, $question_id, $pass)
1165  );
1166  if ($result->numRows() == 1) {
1167  $row = $ilDB->fetchAssoc($result);
1168  $points = $row["points"];
1169  }
1170  return $points;
1171  }
1172 
1181  public function getReachedPoints($active_id, $pass = null)
1182  {
1183  return round(self::_getReachedPoints($active_id, $this->getId(), $pass), 2);
1184  }
1185 
1192  public function getMaximumPoints()
1193  {
1194  return $this->points;
1195  }
1196 
1208  final public function getAdjustedReachedPoints($active_id, $pass = null, $authorizedSolution = true)
1209  {
1210  if (is_null($pass)) {
1211  include_once "./Modules/Test/classes/class.ilObjTest.php";
1212  $pass = ilObjTest::_getPass($active_id);
1213  }
1214 
1215  // determine reached points for submitted solution
1216  $reached_points = $this->calculateReachedPoints($active_id, $pass, $authorizedSolution);
1217 
1218 
1219 
1220  // deduct points for requested hints from reached points
1221  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintTracking.php';
1222  $hintTracking = new ilAssQuestionHintTracking($this->getId(), $active_id, $pass);
1223  $requestsStatisticData = $hintTracking->getRequestStatisticDataByQuestionAndTestpass();
1224  $reached_points = $reached_points - $requestsStatisticData->getRequestsPoints();
1225 
1226  // adjust reached points regarding to tests scoring options
1227  $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id, $pass);
1228 
1229  return $reached_points;
1230  }
1231 
1241  final public function calculateResultsFromSolution($active_id, $pass = null, $obligationsEnabled = false)
1242  {
1243  global $DIC;
1244  $ilDB = $DIC['ilDB'];
1245  $ilUser = $DIC['ilUser'];
1246 
1247  if (is_null($pass)) {
1248  include_once "./Modules/Test/classes/class.ilObjTest.php";
1249  $pass = ilObjTest::_getPass($active_id);
1250  }
1251 
1252  // determine reached points for submitted solution
1253  $reached_points = $this->calculateReachedPoints($active_id, $pass);
1254 
1255  // deduct points for requested hints from reached points
1256  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintTracking.php';
1257  $questionHintTracking = new ilAssQuestionHintTracking($this->getId(), $active_id, $pass);
1258  $requestsStatisticData = $questionHintTracking->getRequestStatisticDataByQuestionAndTestpass();
1259  $reached_points = $reached_points - $requestsStatisticData->getRequestsPoints();
1260 
1261  // adjust reached points regarding to tests scoring options
1262  $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id, $pass);
1263 
1264  if ($obligationsEnabled && ilObjTest::isQuestionObligatory($this->getId())) {
1265  $isAnswered = $this->isAnswered($active_id, $pass);
1266  } else {
1267  $isAnswered = true;
1268  }
1269 
1270  if (is_null($reached_points)) {
1271  $reached_points = 0;
1272  }
1273 
1274  // fau: testNav - check for existing authorized solution to know if a result record should be written
1275  $existingSolutions = $this->lookupForExistingSolutions($active_id, $pass);
1276 
1277  $this->getProcessLocker()->executeUserQuestionResultUpdateOperation(function () use ($ilDB, $active_id, $pass, $reached_points, $requestsStatisticData, $isAnswered, $existingSolutions) {
1278  $query = "
1279  DELETE FROM tst_test_result
1280 
1281  WHERE active_fi = %s
1282  AND question_fi = %s
1283  AND pass = %s
1284  ";
1285 
1286  $types = array('integer', 'integer', 'integer');
1287  $values = array($active_id, $this->getId(), $pass);
1288 
1289  if ($this->getStep() !== null) {
1290  $query .= "
1291  AND step = %s
1292  ";
1293 
1294  $types[] = 'integer';
1295  $values[] = $this->getStep();
1296  }
1297  $ilDB->manipulateF($query, $types, $values);
1298 
1299  if ($existingSolutions['authorized']) {
1300  $next_id = $ilDB->nextId("tst_test_result");
1301  $fieldData = array(
1302  'test_result_id' => array('integer', $next_id),
1303  'active_fi' => array('integer', $active_id),
1304  'question_fi' => array('integer', $this->getId()),
1305  'pass' => array('integer', $pass),
1306  'points' => array('float', $reached_points),
1307  'tstamp' => array('integer', time()),
1308  'hint_count' => array('integer', $requestsStatisticData->getRequestsCount()),
1309  'hint_points' => array('float', $requestsStatisticData->getRequestsPoints()),
1310  'answered' => array('integer', $isAnswered)
1311  );
1312 
1313  if ($this->getStep() !== null) {
1314  $fieldData['step'] = array('integer', $this->getStep());
1315  }
1316 
1317  $ilDB->insert('tst_test_result', $fieldData);
1318  }
1319  });
1320  // fau.
1321 
1322  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
1323 
1326  sprintf(
1327  $this->lng->txtlng(
1328  "assessment",
1329  "log_user_answered_question",
1331  ),
1332  $reached_points
1333  ),
1334  $active_id,
1335  $this->getId()
1336  );
1337  }
1338 
1339  // update test pass results
1340  self::_updateTestPassResults($active_id, $pass, $obligationsEnabled, $this->getProcessLocker());
1341 
1342  // Update objective status
1343  include_once 'Modules/Course/classes/class.ilCourseObjectiveResult.php';
1344  ilCourseObjectiveResult::_updateObjectiveResult($ilUser->getId(), $active_id, $this->getId());
1345  }
1346 
1355  final public function persistWorkingState($active_id, $pass = null, $obligationsEnabled = false, $authorized = true)
1356  {
1357  if (!$this->validateSolutionSubmit() && !$this->savePartial()) {
1358  return false;
1359  }
1360 
1361  $saveStatus = false;
1362 
1363  $this->getProcessLocker()->executePersistWorkingStateLockOperation(function () use ($active_id, $pass, $authorized, $obligationsEnabled, &$saveStatus) {
1364  if ($pass === null) {
1365  require_once 'Modules/Test/classes/class.ilObjTest.php';
1366  $pass = ilObjTest::_getPass($active_id);
1367  }
1368 
1369  $saveStatus = $this->saveWorkingData($active_id, $pass, $authorized);
1370 
1371  if ($authorized) {
1372  // fau: testNav - remove an intermediate solution if the authorized solution is saved
1373  // the intermediate solution would set the displayed question status as "editing ..."
1374  $this->removeIntermediateSolution($active_id, $pass);
1375  // fau.
1376  $this->calculateResultsFromSolution($active_id, $pass, $obligationsEnabled);
1377  }
1378  });
1379 
1380  return $saveStatus;
1381  }
1382 
1386  final public function persistPreviewState(ilAssQuestionPreviewSession $previewSession)
1387  {
1388  $this->savePreviewData($previewSession);
1389  return $this->validateSolutionSubmit();
1390  }
1391 
1392  public function validateSolutionSubmit()
1393  {
1394  return true;
1395  }
1396 
1406  abstract public function saveWorkingData($active_id, $pass = null, $authorized = true);
1407 
1408  protected function savePreviewData(ilAssQuestionPreviewSession $previewSession)
1409  {
1410  $previewSession->setParticipantsSolution($this->getSolutionSubmit());
1411  }
1412 
1414  public static function _updateTestResultCache($active_id, ilAssQuestionProcessLocker $processLocker = null)
1415  {
1416  global $DIC;
1417  $ilDB = $DIC['ilDB'];
1418 
1419  include_once "./Modules/Test/classes/class.ilObjTest.php";
1420  include_once "./Modules/Test/classes/class.assMarkSchema.php";
1421 
1422  $pass = ilObjTest::_getResultPass($active_id);
1423 
1424  if ($pass !== null) {
1425  $query = "
1426  SELECT tst_pass_result.*
1427  FROM tst_pass_result
1428  WHERE active_fi = %s
1429  AND pass = %s
1430  ";
1431 
1432  $result = $ilDB->queryF(
1433  $query,
1434  array('integer', 'integer'),
1435  array($active_id, $pass)
1436  );
1437 
1438  $row_result = $ilDB->fetchAssoc($result);
1439 
1440  $max = $row_result['maxpoints'];
1441  $reached = $row_result['points'];
1442 
1443  $obligationsAnswered = (int) $row_result['obligations_answered'];
1444 
1445  include_once "./Modules/Test/classes/class.assMarkSchema.php";
1446 
1447  $percentage = (!$max) ? 0 : ($reached / $max) * 100.0;
1448 
1449  $mark = ASS_MarkSchema::_getMatchingMarkFromActiveId($active_id, $percentage);
1450 
1451  $isPassed = ($mark["passed"] ? 1 : 0);
1452  $isFailed = (!$mark["passed"] ? 1 : 0);
1453 
1454  $userTestResultUpdateCallback = function () use (
1455  $ilDB,
1456  $active_id,
1457  $pass,
1458  $max,
1459  $reached,
1460  $isFailed,
1461  $isPassed,
1462  $obligationsAnswered,
1463  $row_result,
1464  $mark
1465  ) {
1466  $passedOnceBefore = 0;
1467  $query = "SELECT passed_once FROM tst_result_cache WHERE active_fi = %s";
1468  $res = $ilDB->queryF($query, array('integer'), array($active_id));
1469  while ($row = $ilDB->fetchAssoc($res)) {
1470  $passedOnceBefore = (int) $row['passed_once'];
1471  }
1472 
1473  $passedOnce = (int) ($isPassed || $passedOnceBefore);
1474 
1475  $ilDB->manipulateF(
1476  "DELETE FROM tst_result_cache WHERE active_fi = %s",
1477  array('integer'),
1478  array($active_id)
1479  );
1480 
1481  $ilDB->insert('tst_result_cache', array(
1482  'active_fi' => array('integer', $active_id),
1483  'pass' => array('integer', strlen($pass) ? $pass : 0),
1484  'max_points' => array('float', strlen($max) ? $max : 0),
1485  'reached_points' => array('float', strlen($reached) ? $reached : 0),
1486  'mark_short' => array('text', strlen($mark["short_name"]) ? $mark["short_name"] : " "),
1487  'mark_official' => array('text', strlen($mark["official_name"]) ? $mark["official_name"] : " "),
1488  'passed_once' => array('integer', $passedOnce),
1489  'passed' => array('integer', $isPassed),
1490  'failed' => array('integer', $isFailed),
1491  'tstamp' => array('integer', time()),
1492  'hint_count' => array('integer', $row_result['hint_count']),
1493  'hint_points' => array('float', $row_result['hint_points']),
1494  'obligations_answered' => array('integer', $obligationsAnswered)
1495  ));
1496  };
1497 
1498  if (is_object($processLocker)) {
1499  $processLocker->executeUserTestResultUpdateLockOperation($userTestResultUpdateCallback);
1500  } else {
1501  $userTestResultUpdateCallback();
1502  }
1503  }
1504  }
1505 
1507  public static function _updateTestPassResults($active_id, $pass, $obligationsEnabled = false, ilAssQuestionProcessLocker $processLocker = null, $test_obj_id = null)
1508  {
1509  global $DIC;
1510  $ilDB = $DIC['ilDB'];
1511 
1512  include_once "./Modules/Test/classes/class.ilObjTest.php";
1513 
1514  if (self::getResultGateway() !== null) {
1515  $data = self::getResultGateway()->getQuestionCountAndPointsForPassOfParticipant($active_id, $pass);
1516  $time = self::getResultGateway()->getWorkingTimeOfParticipantForPass($active_id, $pass);
1517  } else {
1520  }
1521 
1522 
1523  // update test pass results
1524 
1525  $result = $ilDB->queryF(
1526  "
1527  SELECT SUM(points) reachedpoints,
1528  SUM(hint_count) hint_count,
1529  SUM(hint_points) hint_points,
1530  COUNT(DISTINCT(question_fi)) answeredquestions
1531  FROM tst_test_result
1532  WHERE active_fi = %s
1533  AND pass = %s
1534  ",
1535  array('integer','integer'),
1536  array($active_id, $pass)
1537  );
1538 
1539  if ($result->numRows() > 0) {
1540  if ($obligationsEnabled) {
1541  $query = '
1542  SELECT answered answ
1543  FROM tst_test_question
1544  INNER JOIN tst_active
1545  ON active_id = %s
1546  AND tst_test_question.test_fi = tst_active.test_fi
1547  LEFT JOIN tst_test_result
1548  ON tst_test_result.active_fi = %s
1549  AND tst_test_result.pass = %s
1550  AND tst_test_question.question_fi = tst_test_result.question_fi
1551  WHERE obligatory = 1';
1552 
1553  $result_obligatory = $ilDB->queryF(
1554  $query,
1555  array('integer','integer','integer'),
1556  array($active_id, $active_id, $pass)
1557  );
1558 
1559  $obligations_answered = 1;
1560 
1561  while ($row_obligatory = $ilDB->fetchAssoc($result_obligatory)) {
1562  if (!(int) $row_obligatory['answ']) {
1563  $obligations_answered = 0;
1564  break;
1565  }
1566  }
1567  } else {
1568  $obligations_answered = 1;
1569  }
1570 
1571  $row = $ilDB->fetchAssoc($result);
1572 
1573  if ($row['reachedpoints'] === null) {
1574  $row['reachedpoints'] = 0;
1575  }
1576  if ($row['hint_count'] === null) {
1577  $row['hint_count'] = 0;
1578  }
1579  if ($row['hint_points'] === null) {
1580  $row['hint_points'] = 0;
1581  }
1582 
1583  $exam_identifier = ilObjTest::buildExamId($active_id, $pass, $test_obj_id);
1584 
1585  $updatePassResultCallback = function () use ($ilDB, $data, $active_id, $pass, $row, $time, $obligations_answered, $exam_identifier) {
1586 
1588  $ilDB->replace(
1589  'tst_pass_result',
1590  array(
1591  'active_fi' => array('integer', $active_id),
1592  'pass' => array('integer', strlen($pass) ? $pass : 0)),
1593  array(
1594  'points' => array('float', $row['reachedpoints'] ? $row['reachedpoints'] : 0),
1595  'maxpoints' => array('float', $data['points']),
1596  'questioncount' => array('integer', $data['count']),
1597  'answeredquestions' => array('integer', $row['answeredquestions']),
1598  'workingtime' => array('integer', $time),
1599  'tstamp' => array('integer', time()),
1600  'hint_count' => array('integer', $row['hint_count']),
1601  'hint_points' => array('float', $row['hint_points']),
1602  'obligations_answered' => array('integer', $obligations_answered),
1603  'exam_id' => array('text', $exam_identifier)
1604  )
1605  );
1606  };
1607 
1608  if (is_object($processLocker)) {
1609  $processLocker->executeUserPassResultUpdateLockOperation($updatePassResultCallback);
1610  } else {
1611  $updatePassResultCallback();
1612  }
1613  }
1614 
1616 
1617  return array(
1618  'active_fi' => $active_id,
1619  'pass' => $pass,
1620  'points' => ($row["reachedpoints"]) ? $row["reachedpoints"] : 0,
1621  'maxpoints' => $data["points"],
1622  'questioncount' => $data["count"],
1623  'answeredquestions' => $row["answeredquestions"],
1624  'workingtime' => $time,
1625  'tstamp' => time(),
1626  'hint_count' => $row['hint_count'],
1627  'hint_points' => $row['hint_points'],
1628  'obligations_answered' => $obligations_answered,
1629  'exam_id' => $exam_identifier
1630  );
1631  }
1632 
1640  public static function logAction($logtext = "", $active_id = "", $question_id = "")
1641  {
1642  $original_id = "";
1643  if (strlen($question_id)) {
1644  $original_id = self::_getOriginalId($question_id);
1645  }
1646 
1647  require_once 'Modules/Test/classes/class.ilObjAssessmentFolder.php';
1648  require_once 'Modules/Test/classes/class.ilObjTest.php';
1649 
1651  $GLOBALS['DIC']['ilUser']->getId(),
1653  $logtext,
1654  $question_id,
1655  $original_id
1656  );
1657  }
1658 
1666  public function moveUploadedMediaFile($file, $name)
1667  {
1668  $mediatempdir = CLIENT_WEB_DIR . "/assessment/temp";
1669  if (!@is_dir($mediatempdir)) {
1670  ilUtil::createDirectory($mediatempdir);
1671  }
1672  $temp_name = tempnam($mediatempdir, $name . "_____");
1673  $temp_name = str_replace("\\", "/", $temp_name);
1674  @unlink($temp_name);
1675  if (!ilUtil::moveUploadedFile($file, $name, $temp_name)) {
1676  return false;
1677  } else {
1678  return $temp_name;
1679  }
1680  }
1681 
1687  public function getSuggestedSolutionPath()
1688  {
1689  return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/solution/";
1690  }
1691 
1698  public function getJavaPath()
1699  {
1700  return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/java/";
1701  }
1702 
1709  public function getImagePath($question_id = null, $object_id = null)
1710  {
1711  if ($question_id === null) {
1712  $question_id = $this->id;
1713  }
1714 
1715  if ($object_id === null) {
1716  $object_id = $this->obj_id;
1717  }
1718 
1719  return $this->buildImagePath($question_id, $object_id);
1720  }
1721 
1722  public function buildImagePath($questionId, $parentObjectId)
1723  {
1724  return CLIENT_WEB_DIR . "/assessment/{$parentObjectId}/{$questionId}/images/";
1725  }
1726 
1733  public function getFlashPath()
1734  {
1735  return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/flash/";
1736  }
1737 
1744  public function getJavaPathWeb()
1745  {
1746  $relative_path = "assessment/$this->obj_id/$this->id/java/";
1747  return ilObjTest::getDataWebPath($relative_path);
1748  }
1749 
1756  {
1757  $relative_path = "assessment/$this->obj_id/$this->id/solution/";
1758  return ilObjTest::getDataWebPath($relative_path);
1759  }
1760 
1769  public function getImagePathWeb()
1770  {
1771  if (!$this->export_image_path) {
1772  $relative_path = "assessment/$this->obj_id/$this->id/images/";
1773  return ilObjTest::getDataWebPath($relative_path);
1774  } else {
1775  return $this->export_image_path;
1776  }
1777  }
1778 
1785  public function getFlashPathWeb()
1786  {
1787  $relative_path = "assessment/$this->obj_id/$this->id/flash/";
1788  return ilObjTest::getDataWebPath($relative_path);
1789  }
1790 
1791  // hey: prevPassSolutions - accept and prefer intermediate only from current pass
1792  public function getTestOutputSolutions($activeId, $pass)
1793  {
1794  // hey: refactored identifiers
1795  if ($this->getTestPresentationConfig()->isSolutionInitiallyPrefilled()) {
1796  // hey.
1797  return $this->getSolutionValues($activeId, $pass, true);
1798  }
1799 
1800  return $this->getUserSolutionPreferingIntermediate($activeId, $pass);
1801  }
1802  // hey.
1803 
1804  public function getUserSolutionPreferingIntermediate($active_id, $pass = null)
1805  {
1806  $solution = $this->getSolutionValues($active_id, $pass, false);
1807 
1808  if (!count($solution)) {
1809  $solution = $this->getSolutionValues($active_id, $pass, true);
1810  }
1811 
1812  return $solution;
1813  }
1814 
1818  public function getSolutionValues($active_id, $pass = null, $authorized = true)
1819  {
1820  global $DIC;
1821  $ilDB = $DIC['ilDB'];
1822 
1823  if (is_null($pass)) {
1824  $pass = $this->getSolutionMaxPass($active_id);
1825  }
1826 
1827  if ($this->getStep() !== null) {
1828  $query = "
1829  SELECT *
1830  FROM tst_solutions
1831  WHERE active_fi = %s
1832  AND question_fi = %s
1833  AND pass = %s
1834  AND step = %s
1835  AND authorized = %s
1836  ORDER BY solution_id";
1837 
1838  $result = $ilDB->queryF(
1839  $query,
1840  array('integer', 'integer', 'integer', 'integer', 'integer'),
1841  array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
1842  );
1843  } else {
1844  $query = "
1845  SELECT *
1846  FROM tst_solutions
1847  WHERE active_fi = %s
1848  AND question_fi = %s
1849  AND pass = %s
1850  AND authorized = %s
1851  ORDER BY solution_id
1852  ";
1853 
1854  $result = $ilDB->queryF(
1855  $query,
1856  array('integer', 'integer', 'integer', 'integer'),
1857  array($active_id, $this->getId(), $pass, (int) $authorized)
1858  );
1859  }
1860 
1861  $values = array();
1862 
1863  while ($row = $ilDB->fetchAssoc($result)) {
1864  $values[] = $row;
1865  }
1866 
1867  return $values;
1868  }
1869 
1876  public function isInUse($question_id = "")
1877  {
1878  global $DIC;
1879  $ilDB = $DIC['ilDB'];
1880 
1881  if ($question_id < 1) {
1882  $question_id = $this->getId();
1883  }
1884  $result = $ilDB->queryF(
1885  "SELECT COUNT(qpl_questions.question_id) question_count FROM qpl_questions, tst_test_question WHERE qpl_questions.original_id = %s AND qpl_questions.question_id = tst_test_question.question_fi",
1886  array('integer'),
1887  array($question_id)
1888  );
1889  $row = $ilDB->fetchAssoc($result);
1890  $count = $row["question_count"];
1891 
1892  $result = $ilDB->queryF(
1893  "
1894  SELECT tst_active.test_fi
1895  FROM qpl_questions
1896  INNER JOIN tst_test_rnd_qst ON tst_test_rnd_qst.question_fi = qpl_questions.question_id
1897  INNER JOIN tst_active ON tst_active.active_id = tst_test_rnd_qst.active_fi
1898  WHERE qpl_questions.original_id = %s
1899  GROUP BY tst_active.test_fi",
1900  array('integer'),
1901  array($question_id)
1902  );
1903  $count += $result->numRows();
1904 
1905  return $count;
1906  }
1907 
1914  public function isClone($question_id = "")
1915  {
1916  global $DIC;
1917  $ilDB = $DIC['ilDB'];
1918 
1919  if ($question_id < 1) {
1920  $question_id = $this->id;
1921  }
1922  $result = $ilDB->queryF(
1923  "SELECT original_id FROM qpl_questions WHERE question_id = %s",
1924  array('integer'),
1925  array($question_id)
1926  );
1927  $row = $ilDB->fetchAssoc($result);
1928  return ($row["original_id"] > 0) ? true : false;
1929  }
1930 
1937  public function pcArrayShuffle($array)
1938  {
1939  $keys = array_keys($array);
1940  shuffle($keys);
1941  $result = array();
1942  foreach ($keys as $key) {
1943  $result[$key] = $array[$key];
1944  }
1945  return $result;
1946  }
1947 
1953  public static function getQuestionTypeFromDb($question_id)
1954  {
1955  global $DIC;
1956  $ilDB = $DIC['ilDB'];
1957 
1958  $result = $ilDB->queryF(
1959  "SELECT qpl_qst_type.type_tag FROM qpl_qst_type, qpl_questions WHERE qpl_questions.question_id = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id",
1960  array('integer'),
1961  array($question_id)
1962  );
1963  $data = $ilDB->fetchAssoc($result);
1964  return $data["type_tag"];
1965  }
1966 
1973  public function getAdditionalTableName()
1974  {
1975  return "";
1976  }
1977 
1984  public function getAnswerTableName()
1985  {
1986  return "";
1987  }
1988 
1995  public function deleteAnswers($question_id)
1996  {
1997  global $DIC;
1998  $ilDB = $DIC['ilDB'];
1999  $answer_table_name = $this->getAnswerTableName();
2000 
2001  if (!is_array($answer_table_name)) {
2002  $answer_table_name = array($answer_table_name);
2003  }
2004 
2005  foreach ($answer_table_name as $table) {
2006  if (strlen($table)) {
2007  $affectedRows = $ilDB->manipulateF(
2008  "DELETE FROM $table WHERE question_fi = %s",
2009  array('integer'),
2010  array($question_id)
2011  );
2012  }
2013  }
2014  }
2015 
2022  public function deleteAdditionalTableData($question_id)
2023  {
2024  global $DIC;
2025  $ilDB = $DIC['ilDB'];
2026 
2027  $additional_table_name = $this->getAdditionalTableName();
2028 
2029  if (!is_array($additional_table_name)) {
2030  $additional_table_name = array($additional_table_name);
2031  }
2032 
2033  foreach ($additional_table_name as $table) {
2034  if (strlen($table)) {
2035  $affectedRows = $ilDB->manipulateF(
2036  "DELETE FROM $table WHERE question_fi = %s",
2037  array('integer'),
2038  array($question_id)
2039  );
2040  }
2041  }
2042  }
2043 
2050  protected function deletePageOfQuestion($question_id)
2051  {
2052  include_once "./Modules/TestQuestionPool/classes/class.ilAssQuestionPage.php";
2053  $page = new ilAssQuestionPage($question_id);
2054  $page->delete();
2055  return true;
2056  }
2057 
2064  public function delete($question_id)
2065  {
2066  global $DIC;
2067  $ilDB = $DIC['ilDB'];
2068  $ilLog = $DIC['ilLog'];
2069 
2070  if ($question_id < 1) {
2071  return true;
2072  } // nothing to do
2073 
2074  $result = $ilDB->queryF(
2075  "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
2076  array('integer'),
2077  array($question_id)
2078  );
2079  if ($result->numRows() == 1) {
2080  $row = $ilDB->fetchAssoc($result);
2081  $obj_id = $row["obj_fi"];
2082  } else {
2083  return true; // nothing to do
2084  }
2085  try {
2086  $this->deletePageOfQuestion($question_id);
2087  } catch (Exception $e) {
2088  $ilLog->write("EXCEPTION: Could not delete page of question $question_id: $e");
2089  return false;
2090  }
2091 
2092  $affectedRows = $ilDB->manipulateF(
2093  "DELETE FROM qpl_questions WHERE question_id = %s",
2094  array('integer'),
2095  array($question_id)
2096  );
2097  if ($affectedRows == 0) {
2098  return false;
2099  }
2100 
2101  try {
2102  $this->deleteAdditionalTableData($question_id);
2103  $this->deleteAnswers($question_id);
2104  $this->feedbackOBJ->deleteGenericFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
2105  $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
2106  } catch (Exception $e) {
2107  $ilLog->write("EXCEPTION: Could not delete additional table data of question $question_id: $e");
2108  return false;
2109  }
2110 
2111  try {
2112  // delete the question in the tst_test_question table (list of test questions)
2113  $affectedRows = $ilDB->manipulateF(
2114  "DELETE FROM tst_test_question WHERE question_fi = %s",
2115  array('integer'),
2116  array($question_id)
2117  );
2118  } catch (Exception $e) {
2119  $ilLog->write("EXCEPTION: Could not delete delete question $question_id from a test: $e");
2120  return false;
2121  }
2122 
2123  try {
2124  // delete suggested solutions contained in the question
2125  $affectedRows = $ilDB->manipulateF(
2126  "DELETE FROM qpl_sol_sug WHERE question_fi = %s",
2127  array('integer'),
2128  array($question_id)
2129  );
2130  } catch (Exception $e) {
2131  $ilLog->write("EXCEPTION: Could not delete suggested solutions of question $question_id: $e");
2132  return false;
2133  }
2134 
2135  try {
2136  $directory = CLIENT_WEB_DIR . "/assessment/" . $obj_id . "/$question_id";
2137  if (preg_match("/\d+/", $obj_id) and preg_match("/\d+/", $question_id) and is_dir($directory)) {
2138  include_once "./Services/Utilities/classes/class.ilUtil.php";
2139  ilUtil::delDir($directory);
2140  }
2141  } catch (Exception $e) {
2142  $ilLog->write("EXCEPTION: Could not delete question file directory $directory of question $question_id: $e");
2143  return false;
2144  }
2145 
2146  try {
2147  include_once("./Services/MediaObjects/classes/class.ilObjMediaObject.php");
2148  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $question_id);
2149  // remaining usages are not in text anymore -> delete them
2150  // and media objects (note: delete method of ilObjMediaObject
2151  // checks whether object is used in another context; if yes,
2152  // the object is not deleted!)
2153  foreach ($mobs as $mob) {
2154  ilObjMediaObject::_removeUsage($mob, "qpl:html", $question_id);
2155  if (ilObjMediaObject::_exists($mob)) {
2156  $mob_obj = new ilObjMediaObject($mob);
2157  $mob_obj->delete();
2158  }
2159  }
2160  } catch (Exception $e) {
2161  $ilLog->write("EXCEPTION: Error deleting the media objects of question $question_id: $e");
2162  return false;
2163  }
2164 
2165  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintTracking.php';
2166  ilAssQuestionHintTracking::deleteRequestsByQuestionIds(array($question_id));
2167 
2168  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintList.php';
2170 
2171  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionSkillAssignmentList.php';
2172  $assignmentList = new ilAssQuestionSkillAssignmentList($ilDB);
2173  $assignmentList->setParentObjId($obj_id);
2174  $assignmentList->setQuestionIdFilter($question_id);
2175  $assignmentList->loadFromDb();
2176  foreach ($assignmentList->getAssignmentsByQuestionId($question_id) as $assignment) {
2177  /* @var ilAssQuestionSkillAssignment $assignment */
2178  $assignment->deleteFromDb();
2179  }
2180 
2181  $this->deleteTaxonomyAssignments();
2182 
2183  try {
2184  // update question count of question pool
2185  include_once "./Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php";
2187  } catch (Exception $e) {
2188  $ilLog->write("EXCEPTION: Error updating the question pool question count of question pool " . $this->getObjId() . " when deleting question $question_id: $e");
2189  return false;
2190  }
2191 
2192  $this->notifyQuestionDeleted($this);
2193 
2194  return true;
2195  }
2196 
2197  private function deleteTaxonomyAssignments()
2198  {
2199  require_once 'Services/Taxonomy/classes/class.ilObjTaxonomy.php';
2200  require_once 'Services/Taxonomy/classes/class.ilTaxNodeAssignment.php';
2201  $taxIds = ilObjTaxonomy::getUsageOfObject($this->getObjId());
2202 
2203  foreach ($taxIds as $taxId) {
2204  $taxNodeAssignment = new ilTaxNodeAssignment('qpl', $this->getObjId(), 'quest', $taxId);
2205  $taxNodeAssignment->deleteAssignmentsOfItem($this->getId());
2206  }
2207  }
2208 
2212  public function getTotalAnswers()
2213  {
2214  return $this->_getTotalAnswers($this->id);
2215  }
2216 
2223  public function _getTotalAnswers($a_q_id)
2224  {
2225  global $DIC;
2226  $ilDB = $DIC['ilDB'];
2227 
2228  // get all question references to the question id
2229  $result = $ilDB->queryF(
2230  "SELECT question_id FROM qpl_questions WHERE original_id = %s OR question_id = %s",
2231  array('integer','integer'),
2232  array($a_q_id, $a_q_id)
2233  );
2234  if ($result->numRows() == 0) {
2235  return 0;
2236  }
2237  $found_id = array();
2238  while ($row = $ilDB->fetchAssoc($result)) {
2239  array_push($found_id, $row["question_id"]);
2240  }
2241 
2242  $result = $ilDB->query("SELECT * FROM tst_test_result WHERE " . $ilDB->in('question_fi', $found_id, false, 'integer'));
2243 
2244  return $result->numRows();
2245  }
2246 
2247 
2254  public static function _getTotalRightAnswers($a_q_id)
2255  {
2256  global $DIC;
2257  $ilDB = $DIC['ilDB'];
2258  $result = $ilDB->queryF(
2259  "SELECT question_id FROM qpl_questions WHERE original_id = %s OR question_id = %s",
2260  array('integer','integer'),
2261  array($a_q_id, $a_q_id)
2262  );
2263  if ($result->numRows() == 0) {
2264  return 0;
2265  }
2266  $found_id = array();
2267  while ($row = $ilDB->fetchAssoc($result)) {
2268  array_push($found_id, $row["question_id"]);
2269  }
2270  $result = $ilDB->query("SELECT * FROM tst_test_result WHERE " . $ilDB->in('question_fi', $found_id, false, 'integer'));
2271  $answers = array();
2272  while ($row = $ilDB->fetchAssoc($result)) {
2273  $reached = $row["points"];
2274  include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
2275  $max = assQuestion::_getMaximumPoints($row["question_fi"]);
2276  array_push($answers, array("reached" => $reached, "max" => $max));
2277  }
2278  $max = 0.0;
2279  $reached = 0.0;
2280  foreach ($answers as $key => $value) {
2281  $max += $value["max"];
2282  $reached += $value["reached"];
2283  }
2284  if ($max > 0) {
2285  return $reached / $max;
2286  } else {
2287  return 0;
2288  }
2289  }
2290 
2296  public static function _getTitle($a_q_id)
2297  {
2298  global $DIC;
2299  $ilDB = $DIC['ilDB'];
2300  $result = $ilDB->queryF(
2301  "SELECT title FROM qpl_questions WHERE question_id = %s",
2302  array('integer'),
2303  array($a_q_id)
2304  );
2305  if ($result->numRows() == 1) {
2306  $row = $ilDB->fetchAssoc($result);
2307  return $row["title"];
2308  } else {
2309  return "";
2310  }
2311  }
2312 
2318  public static function _getQuestionText($a_q_id)
2319  {
2320  global $DIC;
2321  $ilDB = $DIC['ilDB'];
2322  $result = $ilDB->queryF(
2323  "SELECT question_text FROM qpl_questions WHERE question_id = %s",
2324  array('integer'),
2325  array($a_q_id)
2326  );
2327  if ($result->numRows() == 1) {
2328  $row = $ilDB->fetchAssoc($result);
2329  return $row["question_text"];
2330  } else {
2331  return "";
2332  }
2333  }
2334 
2335  public static function isFileAvailable($file)
2336  {
2337  if (!file_exists($file)) {
2338  return false;
2339  }
2340 
2341  if (!is_file($file)) {
2342  return false;
2343  }
2344 
2345  if (!is_readable($file)) {
2346  return false;
2347  }
2348 
2349  return true;
2350  }
2351 
2352  public function copyXHTMLMediaObjectsOfQuestion($a_q_id)
2353  {
2354  include_once("./Services/MediaObjects/classes/class.ilObjMediaObject.php");
2355  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $a_q_id);
2356  foreach ($mobs as $mob) {
2357  ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->getId());
2358  }
2359  }
2360 
2362  {
2363  include_once("./Services/MediaObjects/classes/class.ilObjMediaObject.php");
2364  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
2365  foreach ($mobs as $mob) {
2366  ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->original_id);
2367  }
2368  }
2369 
2373  public function createPageObject()
2374  {
2375  $qpl_id = $this->getObjId();
2376 
2377  include_once "./Modules/TestQuestionPool/classes/class.ilAssQuestionPage.php";
2378  $this->page = new ilAssQuestionPage(0);
2379  $this->page->setId($this->getId());
2380  $this->page->setParentId($qpl_id);
2381  $this->page->setXMLContent("<PageObject><PageContent>" .
2382  "<Question QRef=\"il__qst_" . $this->getId() . "\"/>" .
2383  "</PageContent></PageObject>");
2384  $this->page->create();
2385  }
2386 
2387  public function copyPageOfQuestion($a_q_id)
2388  {
2389  if ($a_q_id > 0) {
2390  include_once "./Modules/TestQuestionPool/classes/class.ilAssQuestionPage.php";
2391  $page = new ilAssQuestionPage($a_q_id);
2392 
2393  $xml = str_replace("il__qst_" . $a_q_id, "il__qst_" . $this->id, $page->getXMLContent());
2394  $this->page->setXMLContent($xml);
2395  $this->page->updateFromXML();
2396  }
2397  }
2398 
2399  public function getPageOfQuestion()
2400  {
2401  include_once "./Modules/TestQuestionPool/classes/class.ilAssQuestionPage.php";
2402  $page = new ilAssQuestionPage($this->id);
2403  return $page->getXMLContent();
2404  }
2405 
2411  public static function _getQuestionType($question_id)
2412  {
2413  global $DIC;
2414  $ilDB = $DIC['ilDB'];
2415 
2416  if ($question_id < 1) {
2417  return "";
2418  }
2419  $result = $ilDB->queryF(
2420  "SELECT type_tag FROM qpl_questions, qpl_qst_type WHERE qpl_questions.question_id = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id",
2421  array('integer'),
2422  array($question_id)
2423  );
2424  if ($result->numRows() == 1) {
2425  $data = $ilDB->fetchAssoc($result);
2426  return $data["type_tag"];
2427  } else {
2428  return "";
2429  }
2430  }
2431 
2439  public static function _getQuestionTitle($question_id)
2440  {
2441  global $DIC;
2442  $ilDB = $DIC['ilDB'];
2443 
2444  if ($question_id < 1) {
2445  return "";
2446  }
2447 
2448  $result = $ilDB->queryF(
2449  "SELECT title FROM qpl_questions WHERE qpl_questions.question_id = %s",
2450  array('integer'),
2451  array($question_id)
2452  );
2453  if ($result->numRows() == 1) {
2454  $data = $ilDB->fetchAssoc($result);
2455  return $data["title"];
2456  } else {
2457  return "";
2458  }
2459  }
2460 
2461  public function setOriginalId($original_id)
2462  {
2463  $this->original_id = $original_id;
2464  }
2465 
2466  public function getOriginalId()
2467  {
2468  return $this->original_id;
2469  }
2470 
2471  protected static $imageSourceFixReplaceMap = array(
2472  'ok.svg' => 'ok.png', 'not_ok.svg' => 'not_ok.png',
2473  'checkbox_checked.svg' => 'checkbox_checked.png',
2474  'checkbox_unchecked.svg' => 'checkbox_unchecked.png',
2475  'radiobutton_checked.svg' => 'radiobutton_checked.png',
2476  'radiobutton_unchecked.svg' => 'radiobutton_unchecked.png'
2477  );
2478 
2479  public function fixSvgToPng($imageFilenameContainingString)
2480  {
2481  $needles = array_keys(self::$imageSourceFixReplaceMap);
2482  $replacements = array_values(self::$imageSourceFixReplaceMap);
2483  return str_replace($needles, $replacements, $imageFilenameContainingString);
2484  }
2485 
2486 
2487  public function fixUnavailableSkinImageSources($html)
2488  {
2489  $matches = null;
2490  if (preg_match_all('/src="(.*?)"/m', $html, $matches)) {
2491  $sources = $matches[1];
2492 
2493  $needleReplacementMap = array();
2494 
2495  foreach ($sources as $src) {
2496  $file = ilUtil::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH) . DIRECTORY_SEPARATOR . $src;
2497 
2498  if (file_exists($file)) {
2499  continue;
2500  }
2501 
2502  $levels = explode(DIRECTORY_SEPARATOR, $src);
2503  if (count($levels) < 5 || $levels[0] != 'Customizing' || $levels[2] != 'skin') {
2504  continue;
2505  }
2506 
2507  $component = '';
2508 
2509  if ($levels[4] == 'Modules' || $levels[4] == 'Services') {
2510  $component = $levels[4] . DIRECTORY_SEPARATOR . $levels[5];
2511  }
2512 
2513  $needleReplacementMap[$src] = ilUtil::getImagePath(basename($src), $component);
2514  }
2515 
2516  if (count($needleReplacementMap)) {
2517  $html = str_replace(array_keys($needleReplacementMap), array_values($needleReplacementMap), $html);
2518  }
2519  }
2520 
2521  return $html;
2522  }
2523 
2530  public function loadFromDb($question_id)
2531  {
2532  global $DIC;
2533  $ilDB = $DIC['ilDB'];
2534 
2535  $result = $ilDB->queryF(
2536  "SELECT external_id FROM qpl_questions WHERE question_id = %s",
2537  array("integer"),
2538  array($question_id)
2539  );
2540  if ($result->numRows() == 1) {
2541  $data = $ilDB->fetchAssoc($result);
2542  $this->external_id = $data['external_id'];
2543  }
2544 
2545  $result = $ilDB->queryF(
2546  "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
2547  array('integer'),
2548  array($this->getId())
2549  );
2550  $this->suggested_solutions = array();
2551  if ($result->numRows()) {
2552  include_once("./Services/RTE/classes/class.ilRTE.php");
2553  while ($row = $ilDB->fetchAssoc($result)) {
2554  $value = (is_array(unserialize($row["value"]))) ? unserialize($row["value"]) : ilRTE::_replaceMediaObjectImageSrc($row["value"], 1);
2555  $this->suggested_solutions[$row["subquestion_index"]] = array(
2556  "type" => $row["type"],
2557  "value" => $value,
2558  "internal_link" => $row["internal_link"],
2559  "import_id" => $row["import_id"]
2560  );
2561  }
2562  }
2563  }
2564 
2571  public function createNewQuestion($a_create_page = true)
2572  {
2573  global $DIC;
2574  $ilDB = $DIC['ilDB'];
2575  $ilUser = $DIC['ilUser'];
2576 
2577  $complete = "0";
2578  $estw_time = $this->getEstimatedWorkingTime();
2579  $estw_time = sprintf("%02d:%02d:%02d", $estw_time['h'], $estw_time['m'], $estw_time['s']);
2580  $obj_id = ($this->getObjId() <= 0) ? (ilObject::_lookupObjId((strlen($_GET["ref_id"])) ? $_GET["ref_id"] : $_POST["sel_qpl"])) : $this->getObjId();
2581  if ($obj_id > 0) {
2582  if ($a_create_page) {
2583  $tstamp = 0;
2584  } else {
2585  // question pool must not try to purge
2586  $tstamp = time();
2587  }
2588 
2589  $next_id = $ilDB->nextId('qpl_questions');
2590  $affectedRows = $ilDB->insert("qpl_questions", array(
2591  "question_id" => array("integer", $next_id),
2592  "question_type_fi" => array("integer", $this->getQuestionTypeID()),
2593  "obj_fi" => array("integer", $obj_id),
2594  "title" => array("text", null),
2595  "description" => array("text", null),
2596  "author" => array("text", $this->getAuthor()),
2597  "owner" => array("integer", $ilUser->getId()),
2598  "question_text" => array("clob", null),
2599  "points" => array("float", 0),
2600  "nr_of_tries" => array("integer", $this->getDefaultNrOfTries()), // #10771
2601  "working_time" => array("text", $estw_time),
2602  "complete" => array("text", $complete),
2603  "created" => array("integer", time()),
2604  "original_id" => array("integer", null),
2605  "tstamp" => array("integer", $tstamp),
2606  "external_id" => array("text", $this->getExternalId()),
2607  'add_cont_edit_mode' => array('text', $this->getAdditionalContentEditingMode())
2608  ));
2609  $this->setId($next_id);
2610 
2611  if ($a_create_page) {
2612  // create page object of question
2613  $this->createPageObject();
2614  }
2615  }
2616 
2617  $this->notifyQuestionCreated();
2618 
2619  return $this->getId();
2620  }
2621 
2622  public function saveQuestionDataToDb($original_id = "")
2623  {
2624  global $DIC;
2625  $ilDB = $DIC['ilDB'];
2626 
2627  $estw_time = $this->getEstimatedWorkingTime();
2628  $estw_time = sprintf("%02d:%02d:%02d", $estw_time['h'], $estw_time['m'], $estw_time['s']);
2629 
2630  // cleanup RTE images which are not inserted into the question text
2631  include_once("./Services/RTE/classes/class.ilRTE.php");
2632  if ($this->getId() == -1) {
2633  // Neuen Datensatz schreiben
2634  $next_id = $ilDB->nextId('qpl_questions');
2635  $affectedRows = $ilDB->insert("qpl_questions", array(
2636  "question_id" => array("integer", $next_id),
2637  "question_type_fi" => array("integer", $this->getQuestionTypeID()),
2638  "obj_fi" => array("integer", $this->getObjId()),
2639  "title" => array("text", $this->getTitle()),
2640  "description" => array("text", $this->getComment()),
2641  "author" => array("text", $this->getAuthor()),
2642  "owner" => array("integer", $this->getOwner()),
2643  "question_text" => array("clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)),
2644  "points" => array("float", $this->getMaximumPoints()),
2645  "working_time" => array("text", $estw_time),
2646  "nr_of_tries" => array("integer", $this->getNrOfTries()),
2647  "created" => array("integer", time()),
2648  "original_id" => array("integer", ($original_id) ? $original_id : null),
2649  "tstamp" => array("integer", time()),
2650  "external_id" => array("text", $this->getExternalId()),
2651  'add_cont_edit_mode' => array('text', $this->getAdditionalContentEditingMode())
2652  ));
2653  $this->setId($next_id);
2654  // create page object of question
2655  $this->createPageObject();
2656  } else {
2657  // Vorhandenen Datensatz aktualisieren
2658  $affectedRows = $ilDB->update("qpl_questions", array(
2659  "obj_fi" => array("integer", $this->getObjId()),
2660  "title" => array("text", $this->getTitle()),
2661  "description" => array("text", $this->getComment()),
2662  "author" => array("text", $this->getAuthor()),
2663  "question_text" => array("clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)),
2664  "points" => array("float", $this->getMaximumPoints()),
2665  "nr_of_tries" => array("integer", $this->getNrOfTries()),
2666  "working_time" => array("text", $estw_time),
2667  "tstamp" => array("integer", time()),
2668  'complete' => array('integer', $this->isComplete()),
2669  "external_id" => array("text", $this->getExternalId())
2670  ), array(
2671  "question_id" => array("integer", $this->getId())
2672  ));
2673  }
2674  }
2675 
2682  public function saveToDb($original_id = "")
2683  {
2684  global $DIC;
2685 
2686  $this->updateSuggestedSolutions();
2687 
2688  // remove unused media objects from ILIAS
2689  $this->cleanupMediaObjectUsage();
2690 
2691  $complete = "0";
2692  if ($this->isComplete()) {
2693  $complete = "1";
2694  }
2695 
2696  $DIC->database()->update('qpl_questions', array(
2697  'tstamp' => array('integer', time()),
2698  'owner' => array('integer', ($this->getOwner() <= 0 ? $this->ilias->account->id : $this->getOwner())),
2699  'complete' => array('integer', $complete),
2700  'lifecycle' => array('text', $this->getLifecycle()->getIdentifier()),
2701  ), array(
2702  'question_id' => array('integer', $this->getId())
2703  ));
2704 
2705  // update question count of question pool
2706  include_once "./Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php";
2708 
2709  $this->notifyQuestionEdited($this);
2710  }
2711 
2715  public function setNewOriginalId($newId)
2716  {
2717  self::saveOriginalId($this->getId(), $newId);
2718  }
2719 
2720  public static function saveOriginalId($questionId, $originalId)
2721  {
2722  $query = "UPDATE qpl_questions SET tstamp = %s, original_id = %s WHERE question_id = %s";
2723 
2724  $GLOBALS['DIC']['ilDB']->manipulateF(
2725  $query,
2726  array('integer','integer', 'text'),
2727  array(time(), $originalId, $questionId)
2728  );
2729  }
2730 
2731  public static function resetOriginalId($questionId)
2732  {
2733  $query = "UPDATE qpl_questions SET tstamp = %s, original_id = NULL WHERE question_id = %s";
2734 
2735  $GLOBALS['DIC']['ilDB']->manipulateF(
2736  $query,
2737  array('integer', 'text'),
2738  array(time(), $questionId)
2739  );
2740  }
2741 
2745  protected function onDuplicate($originalParentId, $originalQuestionId, $duplicateParentId, $duplicateQuestionId)
2746  {
2747  $this->duplicateSuggestedSolutionFiles($originalParentId, $originalQuestionId);
2748 
2749  // duplicate question feeback
2750  $this->feedbackOBJ->duplicateFeedback($originalQuestionId, $duplicateQuestionId);
2751 
2752  // duplicate question hints
2753  $this->duplicateQuestionHints($originalQuestionId, $duplicateQuestionId);
2754 
2755  // duplicate skill assignments
2756  $this->duplicateSkillAssignments($originalParentId, $originalQuestionId, $duplicateParentId, $duplicateQuestionId);
2757  }
2758 
2759  protected function beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
2760  {
2761  }
2762 
2763  protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
2764  {
2765  // sync question feeback
2766  $this->feedbackOBJ->syncFeedback($origQuestionId, $dupQuestionId);
2767  }
2768 
2772  protected function onCopy($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId)
2773  {
2774  $this->copySuggestedSolutionFiles($sourceParentId, $sourceQuestionId);
2775 
2776  // duplicate question feeback
2777  $this->feedbackOBJ->duplicateFeedback($sourceQuestionId, $targetQuestionId);
2778 
2779  // duplicate question hints
2780  $this->duplicateQuestionHints($sourceQuestionId, $targetQuestionId);
2781 
2782  // duplicate skill assignments
2783  $this->duplicateSkillAssignments($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId);
2784  }
2785 
2789  public function deleteSuggestedSolutions()
2790  {
2791  global $DIC;
2792  $ilDB = $DIC['ilDB'];
2793  // delete the links in the qpl_sol_sug table
2794  $affectedRows = $ilDB->manipulateF(
2795  "DELETE FROM qpl_sol_sug WHERE question_fi = %s",
2796  array('integer'),
2797  array($this->getId())
2798  );
2799  // delete the links in the int_link table
2800  include_once "./Services/Link/classes/class.ilInternalLink.php";
2802  $this->suggested_solutions = array();
2804  }
2805 
2813  public function getSuggestedSolution($subquestion_index = 0)
2814  {
2815  if (array_key_exists($subquestion_index, $this->suggested_solutions)) {
2816  return $this->suggested_solutions[$subquestion_index];
2817  } else {
2818  return array();
2819  }
2820  }
2821 
2830  public function getSuggestedSolutionTitle($subquestion_index = 0)
2831  {
2832  if (array_key_exists($subquestion_index, $this->suggested_solutions)) {
2833  $title = $this->suggested_solutions[$subquestion_index]["internal_link"];
2834  // TO DO: resolve internal link an get link type and title
2835  } else {
2836  $title = "";
2837  }
2838  return $title;
2839  }
2840 
2850  public function setSuggestedSolution($solution_id = "", $subquestion_index = 0, $is_import = false)
2851  {
2852  if (strcmp($solution_id, "") != 0) {
2853  $import_id = "";
2854  if ($is_import) {
2855  $import_id = $solution_id;
2856  $solution_id = $this->_resolveInternalLink($import_id);
2857  }
2858  $this->suggested_solutions[$subquestion_index] = array(
2859  "internal_link" => $solution_id,
2860  "import_id" => $import_id
2861  );
2862  }
2863  }
2864 
2868  protected function duplicateSuggestedSolutionFiles($parent_id, $question_id)
2869  {
2870  global $DIC;
2871  $ilLog = $DIC['ilLog'];
2872 
2873  foreach ($this->suggested_solutions as $index => $solution) {
2874  if (strcmp($solution["type"], "file") == 0) {
2875  $filepath = $this->getSuggestedSolutionPath();
2876  $filepath_original = str_replace(
2877  "/{$this->obj_id}/{$this->id}/solution",
2878  "/$parent_id/$question_id/solution",
2879  $filepath
2880  );
2881  if (!file_exists($filepath)) {
2882  ilUtil::makeDirParents($filepath);
2883  }
2884  $filename = $solution["value"]["name"];
2885  if (strlen($filename)) {
2886  if (!copy($filepath_original . $filename, $filepath . $filename)) {
2887  $ilLog->write("File could not be duplicated!!!!", $ilLog->ERROR);
2888  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
2889  }
2890  }
2891  }
2892  }
2893  }
2894 
2899  {
2900  global $DIC;
2901  $ilLog = $DIC['ilLog'];
2902 
2903  $filepath = $this->getSuggestedSolutionPath();
2904  $filepath_original = str_replace("/$this->id/solution", "/$original_id/solution", $filepath);
2905  ilUtil::delDir($filepath_original);
2906  foreach ($this->suggested_solutions as $index => $solution) {
2907  if (strcmp($solution["type"], "file") == 0) {
2908  if (!file_exists($filepath_original)) {
2909  ilUtil::makeDirParents($filepath_original);
2910  }
2911  $filename = $solution["value"]["name"];
2912  if (strlen($filename)) {
2913  if (!@copy($filepath . $filename, $filepath_original . $filename)) {
2914  $ilLog->write("File could not be duplicated!!!!", $ilLog->ERROR);
2915  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
2916  }
2917  }
2918  }
2919  }
2920  }
2921 
2922  protected function copySuggestedSolutionFiles($source_questionpool_id, $source_question_id)
2923  {
2924  global $DIC;
2925  $ilLog = $DIC['ilLog'];
2926 
2927  foreach ($this->suggested_solutions as $index => $solution) {
2928  if (strcmp($solution["type"], "file") == 0) {
2929  $filepath = $this->getSuggestedSolutionPath();
2930  $filepath_original = str_replace("/$this->obj_id/$this->id/solution", "/$source_questionpool_id/$source_question_id/solution", $filepath);
2931  if (!file_exists($filepath)) {
2932  ilUtil::makeDirParents($filepath);
2933  }
2934  $filename = $solution["value"]["name"];
2935  if (strlen($filename)) {
2936  if (!copy($filepath_original . $filename, $filepath . $filename)) {
2937  $ilLog->write("File could not be copied!!!!", $ilLog->ERROR);
2938  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
2939  }
2940  }
2941  }
2942  }
2943  }
2944 
2948  public function updateSuggestedSolutions($original_id = "")
2949  {
2950  global $DIC;
2951  $ilDB = $DIC['ilDB'];
2952 
2953  $id = (strlen($original_id) && is_numeric($original_id)) ? $original_id : $this->getId();
2954  include_once "./Services/Link/classes/class.ilInternalLink.php";
2955  $affectedRows = $ilDB->manipulateF(
2956  "DELETE FROM qpl_sol_sug WHERE question_fi = %s",
2957  array('integer'),
2958  array($id)
2959  );
2961  include_once("./Services/RTE/classes/class.ilRTE.php");
2962  foreach ($this->suggested_solutions as $index => $solution) {
2963  $next_id = $ilDB->nextId('qpl_sol_sug');
2965  $ilDB->insert(
2966  'qpl_sol_sug',
2967  array(
2968  'suggested_solution_id' => array( 'integer', $next_id ),
2969  'question_fi' => array( 'integer', $id ),
2970  'type' => array( 'text', $solution['type'] ),
2971  'value' => array( 'clob', ilRTE::_replaceMediaObjectImageSrc((is_array($solution['value'])) ? serialize($solution[ 'value' ]) : $solution['value'], 0) ),
2972  'internal_link' => array( 'text', $solution['internal_link'] ),
2973  'import_id' => array( 'text', null ),
2974  'subquestion_index' => array( 'integer', $index ),
2975  'tstamp' => array( 'integer', time() ),
2976  )
2977  );
2978  if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $solution["internal_link"], $matches)) {
2979  ilInternalLink::_saveLink("qst", $id, $matches[2], $matches[3], $matches[1]);
2980  }
2981  }
2982  if (strlen($original_id) && is_numeric($original_id)) {
2984  }
2985  $this->cleanupMediaObjectUsage();
2986  }
2987 
2997  public function saveSuggestedSolution($type, $solution_id = "", $subquestion_index = 0, $value = "")
2998  {
2999  global $DIC;
3000  $ilDB = $DIC['ilDB'];
3001 
3002  $affectedRows = $ilDB->manipulateF(
3003  "DELETE FROM qpl_sol_sug WHERE question_fi = %s AND subquestion_index = %s",
3004  array("integer", "integer"),
3005  array(
3006  $this->getId(),
3007  $subquestion_index
3008  )
3009  );
3010 
3011  $next_id = $ilDB->nextId('qpl_sol_sug');
3012  include_once("./Services/RTE/classes/class.ilRTE.php");
3014  $affectedRows = $ilDB->insert(
3015  'qpl_sol_sug',
3016  array(
3017  'suggested_solution_id' => array( 'integer', $next_id ),
3018  'question_fi' => array( 'integer', $this->getId() ),
3019  'type' => array( 'text', $type ),
3020  'value' => array( 'clob', ilRTE::_replaceMediaObjectImageSrc((is_array($value)) ? serialize($value) : $value, 0) ),
3021  'internal_link' => array( 'text', $solution_id ),
3022  'import_id' => array( 'text', null ),
3023  'subquestion_index' => array( 'integer', $subquestion_index ),
3024  'tstamp' => array( 'integer', time() ),
3025  )
3026  );
3027  if ($affectedRows == 1) {
3028  $this->suggested_solutions[$subquestion_index] = array(
3029  "type" => $type,
3030  "value" => $value,
3031  "internal_link" => $solution_id,
3032  "import_id" => ""
3033  );
3034  }
3035  $this->cleanupMediaObjectUsage();
3036  }
3037 
3038  public function _resolveInternalLink($internal_link)
3039  {
3040  if (preg_match("/il_(\d+)_(\w+)_(\d+)/", $internal_link, $matches)) {
3041  switch ($matches[2]) {
3042  case "lm":
3043  $resolved_link = ilLMObject::_getIdForImportId($internal_link);
3044  break;
3045  case "pg":
3046  $resolved_link = ilInternalLink::_getIdForImportId("PageObject", $internal_link);
3047  break;
3048  case "st":
3049  $resolved_link = ilInternalLink::_getIdForImportId("StructureObject", $internal_link);
3050  break;
3051  case "git":
3052  $resolved_link = ilInternalLink::_getIdForImportId("GlossaryItem", $internal_link);
3053  break;
3054  case "mob":
3055  $resolved_link = ilInternalLink::_getIdForImportId("MediaObject", $internal_link);
3056  break;
3057  }
3058  if (strcmp($resolved_link, "") == 0) {
3059  $resolved_link = $internal_link;
3060  }
3061  } else {
3062  $resolved_link = $internal_link;
3063  }
3064  return $resolved_link;
3065  }
3066 
3067  public function _resolveIntLinks($question_id)
3068  {
3069  global $DIC;
3070  $ilDB = $DIC['ilDB'];
3071  $resolvedlinks = 0;
3072  $result = $ilDB->queryF(
3073  "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
3074  array('integer'),
3075  array($question_id)
3076  );
3077  if ($result->numRows()) {
3078  while ($row = $ilDB->fetchAssoc($result)) {
3079  $internal_link = $row["internal_link"];
3080  include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
3081  $resolved_link = assQuestion::_resolveInternalLink($internal_link);
3082  if (strcmp($internal_link, $resolved_link) != 0) {
3083  // internal link was resolved successfully
3084  $affectedRows = $ilDB->manipulateF(
3085  "UPDATE qpl_sol_sug SET internal_link = %s WHERE suggested_solution_id = %s",
3086  array('text','integer'),
3087  array($resolved_link, $row["suggested_solution_id"])
3088  );
3089  $resolvedlinks++;
3090  }
3091  }
3092  }
3093  if ($resolvedlinks) {
3094  // there are resolved links -> reenter theses links to the database
3095 
3096  // delete all internal links from the database
3097  include_once "./Services/Link/classes/class.ilInternalLink.php";
3098  ilInternalLink::_deleteAllLinksOfSource("qst", $question_id);
3099 
3100  $result = $ilDB->queryF(
3101  "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
3102  array('integer'),
3103  array($question_id)
3104  );
3105  if ($result->numRows()) {
3106  while ($row = $ilDB->fetchAssoc($result)) {
3107  if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $row["internal_link"], $matches)) {
3108  ilInternalLink::_saveLink("qst", $question_id, $matches[2], $matches[3], $matches[1]);
3109  }
3110  }
3111  }
3112  }
3113  }
3114 
3115  public static function _getInternalLinkHref($target = "")
3116  {
3117  global $DIC;
3118  $ilDB = $DIC['ilDB'];
3119  $linktypes = array(
3120  "lm" => "LearningModule",
3121  "pg" => "PageObject",
3122  "st" => "StructureObject",
3123  "git" => "GlossaryItem",
3124  "mob" => "MediaObject"
3125  );
3126  $href = "";
3127  if (preg_match("/il__(\w+)_(\d+)/", $target, $matches)) {
3128  $type = $matches[1];
3129  $target_id = $matches[2];
3130  include_once "./Services/Utilities/classes/class.ilUtil.php";
3131  switch ($linktypes[$matches[1]]) {
3132  case "LearningModule":
3133  $href = "./goto.php?target=" . $type . "_" . $target_id;
3134  break;
3135  case "PageObject":
3136  case "StructureObject":
3137  $href = "./goto.php?target=" . $type . "_" . $target_id;
3138  break;
3139  case "GlossaryItem":
3140  $href = "./goto.php?target=" . $type . "_" . $target_id;
3141  break;
3142  case "MediaObject":
3143  $href = "./ilias.php?baseClass=ilLMPresentationGUI&obj_type=" . $linktypes[$type] . "&cmd=media&ref_id=" . $_GET["ref_id"] . "&mob_id=" . $target_id;
3144  break;
3145  }
3146  }
3147  return $href;
3148  }
3149 
3157  public static function _getOriginalId($question_id)
3158  {
3159  global $DIC;
3160  $ilDB = $DIC['ilDB'];
3161  $result = $ilDB->queryF(
3162  "SELECT * FROM qpl_questions WHERE question_id = %s",
3163  array('integer'),
3164  array($question_id)
3165  );
3166  if ($result->numRows() > 0) {
3167  $row = $ilDB->fetchAssoc($result);
3168  if ($row["original_id"] > 0) {
3169  return $row["original_id"];
3170  } else {
3171  return $row["question_id"];
3172  }
3173  } else {
3174  return "";
3175  }
3176  }
3177 
3178  public static function originalQuestionExists($questionId)
3179  {
3180  global $DIC;
3181  $ilDB = $DIC['ilDB'];
3182 
3183  $query = "
3184  SELECT COUNT(dupl.question_id) cnt
3185  FROM qpl_questions dupl
3186  INNER JOIN qpl_questions orig
3187  ON orig.question_id = dupl.original_id
3188  WHERE dupl.question_id = %s
3189  ";
3190 
3191  $res = $ilDB->queryF($query, array('integer'), array($questionId));
3192  $row = $ilDB->fetchAssoc($res);
3193 
3194  return $row['cnt'] > 0;
3195  }
3196 
3197  public function syncWithOriginal()
3198  {
3199  global $DIC;
3200  $ilDB = $DIC['ilDB'];
3201 
3202  if (!$this->getOriginalId()) {
3203  return;
3204  }
3205 
3206  $originalObjId = self::lookupOriginalParentObjId($this->getOriginalId());
3207 
3208  if (!$originalObjId) {
3209  return;
3210  }
3211 
3212  $id = $this->getId();
3213  $objId = $this->getObjId();
3214  $original = $this->getOriginalId();
3215 
3216  $this->beforeSyncWithOriginal($original, $id, $originalObjId, $objId);
3217 
3218  $this->setId($original);
3219  $this->setOriginalId(null);
3220  $this->setObjId($originalObjId);
3221 
3222  $this->saveToDb();
3223 
3224  $this->deletePageOfQuestion($original);
3225  $this->createPageObject();
3226  $this->copyPageOfQuestion($id);
3227 
3228  $this->setId($id);
3229  $this->setOriginalId($original);
3230  $this->setObjId($objId);
3231 
3232  $this->updateSuggestedSolutions($original);
3234 
3235  $this->afterSyncWithOriginal($original, $id, $originalObjId, $objId);
3236  $this->syncHints();
3237  }
3238 
3246  public function _questionExists($question_id)
3247  {
3248  global $DIC;
3249  $ilDB = $DIC['ilDB'];
3250 
3251  if ($question_id < 1) {
3252  return false;
3253  }
3254 
3255  $result = $ilDB->queryF(
3256  "SELECT question_id FROM qpl_questions WHERE question_id = %s",
3257  array('integer'),
3258  array($question_id)
3259  );
3260  if ($result->numRows() == 1) {
3261  return true;
3262  } else {
3263  return false;
3264  }
3265  }
3266 
3274  public function _questionExistsInPool($question_id)
3275  {
3276  global $DIC;
3277  $ilDB = $DIC['ilDB'];
3278 
3279  if ($question_id < 1) {
3280  return false;
3281  }
3282 
3283  $result = $ilDB->queryF(
3284  "SELECT question_id FROM qpl_questions INNER JOIN object_data ON obj_fi = obj_id WHERE question_id = %s AND type = 'qpl'",
3285  array('integer'),
3286  array($question_id)
3287  );
3288  if ($result->numRows() == 1) {
3289  return true;
3290  } else {
3291  return false;
3292  }
3293  }
3294 
3302  public static function _instanciateQuestion($question_id)
3303  {
3304  return self::_instantiateQuestion($question_id);
3305  }
3306 
3311  public static function _instantiateQuestion($question_id)
3312  {
3313  global $DIC;
3314  $ilCtrl = $DIC['ilCtrl'];
3315  $ilDB = $DIC['ilDB'];
3316  $lng = $DIC['lng'];
3317 
3318  if (strcmp($question_id, "") != 0) {
3319  $question_type = assQuestion::_getQuestionType($question_id);
3320  if (!strlen($question_type)) {
3321  return null;
3322  }
3323  assQuestion::_includeClass($question_type);
3324  $objectClassname = self::getObjectClassNameByQuestionType($question_type);
3325  $question = new $objectClassname();
3326  $question->loadFromDb($question_id);
3327 
3328  $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
3329  $question->feedbackOBJ = new $feedbackObjectClassname($question, $ilCtrl, $ilDB, $lng);
3330 
3331  return $question;
3332  }
3333  }
3334 
3341  public function getPoints()
3342  {
3343  if (strcmp($this->points, "") == 0) {
3344  return 0;
3345  } else {
3346  return $this->points;
3347  }
3348  }
3349 
3350 
3357  public function setPoints($a_points)
3358  {
3359  $this->points = $a_points;
3360  }
3361 
3368  public function getSolutionMaxPass($active_id)
3369  {
3370  return self::_getSolutionMaxPass($this->getId(), $active_id);
3371  }
3372 
3379  public static function _getSolutionMaxPass($question_id, $active_id)
3380  {
3381  /* include_once "./Modules/Test/classes/class.ilObjTest.php";
3382  $pass = ilObjTest::_getPass($active_id);
3383  return $pass;*/
3384 
3385  // the following code was the old solution which added the non answered
3386  // questions of a pass from the answered questions of the previous pass
3387  // with the above solution, only the answered questions of the last pass are counted
3388  global $DIC;
3389  $ilDB = $DIC['ilDB'];
3390 
3391  $result = $ilDB->queryF(
3392  "SELECT MAX(pass) maxpass FROM tst_test_result WHERE active_fi = %s AND question_fi = %s",
3393  array('integer','integer'),
3394  array($active_id, $question_id)
3395  );
3396  if ($result->numRows() == 1) {
3397  $row = $ilDB->fetchAssoc($result);
3398  return $row["maxpass"];
3399  } else {
3400  return 0;
3401  }
3402  }
3403 
3412  public static function _isWriteable($question_id, $user_id)
3413  {
3414  global $DIC;
3415  $ilDB = $DIC['ilDB'];
3416 
3417  if (($question_id < 1) || ($user_id < 1)) {
3418  return false;
3419  }
3420 
3421  $result = $ilDB->queryF(
3422  "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
3423  array('integer'),
3424  array($question_id)
3425  );
3426  if ($result->numRows() == 1) {
3427  $row = $ilDB->fetchAssoc($result);
3428  $qpl_object_id = $row["obj_fi"];
3429  include_once "./Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php";
3430  return ilObjQuestionPool::_isWriteable($qpl_object_id, $user_id);
3431  } else {
3432  return false;
3433  }
3434  }
3435 
3442  public static function _isUsedInRandomTest($question_id = "")
3443  {
3444  global $DIC;
3445  $ilDB = $DIC['ilDB'];
3446 
3447  if ($question_id < 1) {
3448  return 0;
3449  }
3450  $result = $ilDB->queryF(
3451  "SELECT test_random_question_id FROM tst_test_rnd_qst WHERE question_fi = %s",
3452  array('integer'),
3453  array($question_id)
3454  );
3455  return $result->numRows();
3456  }
3457 
3469  abstract public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false);
3470 
3471  public function deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
3472  {
3473  global $DIC;
3474 
3475  $hintTracking = new ilAssQuestionPreviewHintTracking($DIC->database(), $previewSession);
3476  $requestsStatisticData = $hintTracking->getRequestStatisticData();
3477  $reachedPoints = $reachedPoints - $requestsStatisticData->getRequestsPoints();
3478 
3479  return $reachedPoints;
3480  }
3481 
3483  {
3484  $reachedPoints = $this->calculateReachedPointsForSolution($previewSession->getParticipantsSolution());
3485  $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $reachedPoints);
3486 
3487  return $this->ensureNonNegativePoints($reachedPoints);
3488  }
3489 
3490  protected function ensureNonNegativePoints($points)
3491  {
3492  return $points > 0 ? $points : 0;
3493  }
3494 
3496  {
3497  $reachedPoints = $this->calculateReachedPointsFromPreviewSession($previewSession);
3498 
3499  if ($reachedPoints < $this->getMaximumPoints()) {
3500  return false;
3501  }
3502 
3503  return true;
3504  }
3505 
3506 
3517  final public function adjustReachedPointsByScoringOptions($points, $active_id, $pass = null)
3518  {
3519  include_once "./Modules/Test/classes/class.ilObjTest.php";
3520  $count_system = ilObjTest::_getCountSystem($active_id);
3521  if ($count_system == 1) {
3522  if (abs($this->getMaximumPoints() - $points) > 0.0000000001) {
3523  $points = 0;
3524  }
3525  }
3526  $score_cutting = ilObjTest::_getScoreCutting($active_id);
3527  if ($score_cutting == 0) {
3528  if ($points < 0) {
3529  $points = 0;
3530  }
3531  }
3532  return $points;
3533  }
3534 
3543  public static function _isWorkedThrough($active_id, $question_id, $pass = null)
3544  {
3545  return self::lookupResultRecordExist($active_id, $question_id, $pass);
3546 
3547  // oldschool "workedthru"
3548 
3549  global $DIC;
3550  $ilDB = $DIC['ilDB'];
3551 
3552  $points = 0;
3553  if (is_null($pass)) {
3554  include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
3555  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
3556  }
3557  $result = $ilDB->queryF(
3558  "SELECT solution_id FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
3559  array('integer','integer','integer'),
3560  array($active_id, $question_id, $pass)
3561  );
3562  if ($result->numRows()) {
3563  return true;
3564  } else {
3565  return false;
3566  }
3567  }
3568 
3576  public static function _areAnswered($a_user_id, $a_question_ids)
3577  {
3578  global $DIC;
3579  $ilDB = $DIC['ilDB'];
3580 
3581  $res = $ilDB->queryF(
3582  "SELECT DISTINCT(question_fi) FROM tst_test_result JOIN tst_active " .
3583  "ON (active_id = active_fi) " .
3584  "WHERE " . $ilDB->in('question_fi', $a_question_ids, false, 'integer') .
3585  " AND user_fi = %s",
3586  array('integer'),
3587  array($a_user_id)
3588  );
3589  return ($res->numRows() == count($a_question_ids)) ? true : false;
3590  }
3591 
3600  public function isHTML($a_text)
3601  {
3602  return ilUtil::isHTML($a_text);
3603  }
3604 
3611  public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
3612  {
3613  include_once "./Services/Utilities/classes/class.ilUtil.php";
3614  return ilUtil::prepareTextareaOutput($txt_output, $prepare_for_latex_output, $omitNl2BrWhenTextArea);
3615  }
3616 
3624  public function QTIMaterialToString($a_material)
3625  {
3626  $result = "";
3627  for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
3628  $material = $a_material->getMaterial($i);
3629  if (strcmp($material["type"], "mattext") == 0) {
3630  $result .= $material["material"]->getContent();
3631  }
3632  if (strcmp($material["type"], "matimage") == 0) {
3633  $matimage = $material["material"];
3634  if (preg_match("/(il_([0-9]+)_mob_([0-9]+))/", $matimage->getLabel(), $matches)) {
3635  // import an mediaobject which was inserted using tiny mce
3636  if (!is_array($_SESSION["import_mob_xhtml"])) {
3637  $_SESSION["import_mob_xhtml"] = array();
3638  }
3639  array_push($_SESSION["import_mob_xhtml"], array("mob" => $matimage->getLabel(), "uri" => $matimage->getUri()));
3640  }
3641  }
3642  }
3643  return $result;
3644  }
3645 
3654  public function addQTIMaterial(&$a_xml_writer, $a_material, $close_material_tag = true, $add_mobs = true)
3655  {
3656  include_once "./Services/RTE/classes/class.ilRTE.php";
3657  include_once("./Services/MediaObjects/classes/class.ilObjMediaObject.php");
3658 
3659  $a_xml_writer->xmlStartTag("material");
3660  $attrs = array(
3661  "texttype" => "text/plain"
3662  );
3663  if ($this->isHTML($a_material)) {
3664  $attrs["texttype"] = "text/xhtml";
3665  }
3666  $a_xml_writer->xmlElement("mattext", $attrs, ilRTE::_replaceMediaObjectImageSrc($a_material, 0));
3667  if ($add_mobs) {
3668  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
3669  foreach ($mobs as $mob) {
3670  $moblabel = "il_" . IL_INST_ID . "_mob_" . $mob;
3671  if (strpos($a_material, "mm_$mob") !== false) {
3672  if (ilObjMediaObject::_exists($mob)) {
3673  $mob_obj = new ilObjMediaObject($mob);
3674  $imgattrs = array(
3675  "label" => $moblabel,
3676  "uri" => "objects/" . "il_" . IL_INST_ID . "_mob_" . $mob . "/" . $mob_obj->getTitle()
3677  );
3678  }
3679  $a_xml_writer->xmlElement("matimage", $imgattrs, null);
3680  }
3681  }
3682  }
3683  if ($close_material_tag) {
3684  $a_xml_writer->xmlEndTag("material");
3685  }
3686  }
3687 
3688  public function buildHashedImageFilename($plain_image_filename, $unique = false)
3689  {
3690  $extension = "";
3691 
3692  if (preg_match("/.*\.(png|jpg|gif|jpeg)$/i", $plain_image_filename, $matches)) {
3693  $extension = "." . $matches[1];
3694  }
3695 
3696  if ($unique) {
3697  $plain_image_filename = uniqid($plain_image_filename . microtime(true));
3698  }
3699 
3700  $hashed_filename = md5($plain_image_filename) . $extension;
3701 
3702  return $hashed_filename;
3703  }
3704 
3715  public static function _setReachedPoints($active_id, $question_id, $points, $maxpoints, $pass, $manualscoring, $obligationsEnabled)
3716  {
3717  global $DIC;
3718  $ilDB = $DIC['ilDB'];
3719  $refinery = $DIC['refinery'];
3720 
3721  $float_trafo = $refinery->kindlyTo()->float();
3722  try {
3723  $points = $float_trafo->transform($points);
3724  } catch (ILIAS\Refinery\ConstraintViolationException $e) {
3725  return false;
3726  }
3727 
3728  if ($points <= $maxpoints) {
3729  if (is_null($pass)) {
3730  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
3731  }
3732 
3733  // retrieve the already given points
3734  $old_points = 0;
3735  $result = $ilDB->queryF(
3736  "SELECT points FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
3737  array('integer','integer','integer'),
3738  array($active_id, $question_id, $pass)
3739  );
3740  $manual = ($manualscoring) ? 1 : 0;
3741  $rowsnum = $result->numRows();
3742  if ($rowsnum) {
3743  $row = $ilDB->fetchAssoc($result);
3744  $old_points = $row["points"];
3745  if ($old_points != $points) {
3746  $affectedRows = $ilDB->manipulateF(
3747  "UPDATE tst_test_result SET points = %s, manual = %s, tstamp = %s WHERE active_fi = %s AND question_fi = %s AND pass = %s",
3748  array('float', 'integer', 'integer', 'integer', 'integer', 'integer'),
3749  array($points, $manual, time(), $active_id, $question_id, $pass)
3750  );
3751  }
3752  } else {
3753  $next_id = $ilDB->nextId('tst_test_result');
3754  $affectedRows = $ilDB->manipulateF(
3755  "INSERT INTO tst_test_result (test_result_id, active_fi, question_fi, points, pass, manual, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s)",
3756  array('integer', 'integer','integer', 'float', 'integer', 'integer','integer'),
3757  array($next_id, $active_id, $question_id, $points, $pass, $manual, time())
3758  );
3759  }
3760 
3761  if (self::isForcePassResultUpdateEnabled() || $old_points != $points || !$rowsnum) {
3762  assQuestion::_updateTestPassResults($active_id, $pass, $obligationsEnabled);
3763  // finally update objective result
3764  include_once "./Modules/Test/classes/class.ilObjTest.php";
3765  include_once './Modules/Course/classes/class.ilCourseObjectiveResult.php';
3767 
3768  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
3770  global $DIC;
3771  $lng = $DIC['lng'];
3772  $ilUser = $DIC['ilUser'];
3773  include_once "./Modules/Test/classes/class.ilObjTestAccess.php";
3774  $username = ilObjTestAccess::_getParticipantData($active_id);
3775  assQuestion::logAction(sprintf($lng->txtlng("assessment", "log_answer_changed_points", ilObjAssessmentFolder::_getLogLanguage()), $username, $old_points, $points, $ilUser->getFullname() . " (" . $ilUser->getLogin() . ")"), $active_id, $question_id);
3776  }
3777  }
3778 
3779  return true;
3780  } else {
3781  return false;
3782  }
3783  }
3784 
3792  public function getQuestion()
3793  {
3794  return $this->question;
3795  }
3796 
3804  public function setQuestion($question = "")
3805  {
3806  $this->question = $question;
3807  }
3808 
3809  public function getQuestionForHTMLOutput() : string
3810  {
3811  return $this->purifyAndPrepareTextAreaOutput($this->question);
3812  }
3813 
3814  protected function purifyAndPrepareTextAreaOutput(string $content) : string
3815  {
3816  $purified_content = $this->getHtmlQuestionContentPurifier()->purify($content);
3818  || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
3819  $purified_content = nl2br($purified_content);
3820  }
3821  return $this->prepareTextareaOutput(
3822  $purified_content,
3823  true,
3824  true
3825  );
3826  }
3827 
3833  abstract public function getQuestionType();
3834 
3843  public function getQuestionTypeID()
3844  {
3845  global $DIC;
3846  $ilDB = $DIC['ilDB'];
3847 
3848  $result = $ilDB->queryF(
3849  "SELECT question_type_id FROM qpl_qst_type WHERE type_tag = %s",
3850  array('text'),
3851  array($this->getQuestionType())
3852  );
3853  if ($result->numRows() == 1) {
3854  $row = $ilDB->fetchAssoc($result);
3855  return $row["question_type_id"];
3856  }
3857  return 0;
3858  }
3859 
3860  public function syncHints()
3861  {
3862  global $DIC;
3863  $ilDB = $DIC['ilDB'];
3864 
3865  // delete hints of the original
3866  $ilDB->manipulateF(
3867  "DELETE FROM qpl_hints WHERE qht_question_fi = %s",
3868  array('integer'),
3869  array($this->original_id)
3870  );
3871 
3872  // get hints of the actual question
3873  $result = $ilDB->queryF(
3874  "SELECT * FROM qpl_hints WHERE qht_question_fi = %s",
3875  array('integer'),
3876  array($this->getId())
3877  );
3878 
3879  // save hints to the original
3880  if ($result->numRows()) {
3881  while ($row = $ilDB->fetchAssoc($result)) {
3882  $next_id = $ilDB->nextId('qpl_hints');
3884  $ilDB->insert(
3885  'qpl_hints',
3886  array(
3887  'qht_hint_id' => array('integer', $next_id),
3888  'qht_question_fi' => array('integer', $this->original_id),
3889  'qht_hint_index' => array('integer', $row["qht_hint_index"]),
3890  'qht_hint_points' => array('integer', $row["qht_hint_points"]),
3891  'qht_hint_text' => array('text', $row["qht_hint_text"]),
3892  )
3893  );
3894  }
3895  }
3896  }
3897 
3902  protected function getRTETextWithMediaObjects()
3903  {
3904  // must be called in parent classes. add additional RTE text in the parent
3905  // classes and call this method to add the standard RTE text
3906  $collected = $this->getQuestion();
3907  $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), false);
3908  $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), true);
3909  $collected .= $this->feedbackOBJ->getAllSpecificAnswerFeedbackContents($this->getId());
3910 
3911  foreach ($this->suggested_solutions as $solution_array) {
3912  $collected .= $solution_array["value"];
3913  }
3914 
3915  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintList.php';
3916  $questionHintList = ilAssQuestionHintList::getListByQuestionId($this->getId());
3917  foreach ($questionHintList as $questionHint) {
3918  /* @var $questionHint ilAssQuestionHint */
3919  $collected .= $questionHint->getText();
3920  }
3921 
3922  return $collected;
3923  }
3924 
3929  public function cleanupMediaObjectUsage()
3930  {
3931  $combinedtext = $this->getRTETextWithMediaObjects();
3932  include_once("./Services/RTE/classes/class.ilRTE.php");
3933  ilRTE::_cleanupMediaObjectUsage($combinedtext, "qpl:html", $this->getId());
3934  }
3935 
3941  public function &getInstances()
3942  {
3943  global $DIC;
3944  $ilDB = $DIC['ilDB'];
3945 
3946  $result = $ilDB->queryF(
3947  "SELECT question_id FROM qpl_questions WHERE original_id = %s",
3948  array("integer"),
3949  array($this->getId())
3950  );
3951  $instances = array();
3952  $ids = array();
3953  while ($row = $ilDB->fetchAssoc($result)) {
3954  array_push($ids, $row["question_id"]);
3955  }
3956  foreach ($ids as $question_id) {
3957  // check non random tests
3958  $result = $ilDB->queryF(
3959  "SELECT tst_tests.obj_fi FROM tst_tests, tst_test_question WHERE tst_test_question.question_fi = %s AND tst_test_question.test_fi = tst_tests.test_id",
3960  array("integer"),
3961  array($question_id)
3962  );
3963  while ($row = $ilDB->fetchAssoc($result)) {
3964  $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
3965  }
3966  // check random tests
3967  $result = $ilDB->queryF(
3968  "SELECT tst_tests.obj_fi FROM tst_tests, tst_test_rnd_qst, tst_active WHERE tst_test_rnd_qst.active_fi = tst_active.active_id AND tst_test_rnd_qst.question_fi = %s AND tst_tests.test_id = tst_active.test_fi",
3969  array("integer"),
3970  array($question_id)
3971  );
3972  while ($row = $ilDB->fetchAssoc($result)) {
3973  $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
3974  }
3975  }
3976  include_once "./Modules/Test/classes/class.ilObjTest.php";
3977  foreach ($instances as $key => $value) {
3978  $instances[$key] = array("obj_id" => $key, "title" => $value, "author" => ilObjTest::_lookupAuthor($key), "refs" => ilObject::_getAllReferences($key));
3979  }
3980  return $instances;
3981  }
3982 
3983  public static function _needsManualScoring($question_id)
3984  {
3985  include_once "./Modules/Test/classes/class.ilObjAssessmentFolder.php";
3987  $questiontype = assQuestion::_getQuestionType($question_id);
3988  if (in_array($questiontype, $scoring)) {
3989  return true;
3990  } else {
3991  return false;
3992  }
3993  }
3994 
4002  public function getActiveUserData($active_id)
4003  {
4004  global $DIC;
4005  $ilDB = $DIC['ilDB'];
4006  $result = $ilDB->queryF(
4007  "SELECT * FROM tst_active WHERE active_id = %s",
4008  array('integer'),
4009  array($active_id)
4010  );
4011  if ($result->numRows()) {
4012  $row = $ilDB->fetchAssoc($result);
4013  return array("user_id" => $row["user_fi"], "test_id" => $row["test_fi"]);
4014  } else {
4015  return array();
4016  }
4017  }
4018 
4026  public static function _includeClass($question_type, $gui = 0)
4027  {
4028  if (self::isCoreQuestionType($question_type)) {
4029  self::includeCoreClass($question_type, $gui);
4030  } else {
4031  self::includePluginClass($question_type, $gui);
4032  }
4033  }
4034 
4035  public static function getGuiClassNameByQuestionType($questionType)
4036  {
4037  return $questionType . 'GUI';
4038  }
4039 
4040  public static function getObjectClassNameByQuestionType($questionType)
4041  {
4042  return $questionType;
4043  }
4044 
4045  public static function getFeedbackClassNameByQuestionType($questionType)
4046  {
4047  return str_replace('ass', 'ilAss', $questionType) . 'Feedback';
4048  }
4049 
4050  public static function isCoreQuestionType($questionType)
4051  {
4052  $guiClassName = self::getGuiClassNameByQuestionType($questionType);
4053  return file_exists("Modules/TestQuestionPool/classes/class.{$guiClassName}.php");
4054  }
4055 
4056  public static function includeCoreClass($questionType, $withGuiClass)
4057  {
4058  if ($withGuiClass) {
4059  $guiClassName = self::getGuiClassNameByQuestionType($questionType);
4060  require_once "Modules/TestQuestionPool/classes/class.{$guiClassName}.php";
4061 
4062  // object class is included by gui classes constructor
4063  } else {
4064  $objectClassName = self::getObjectClassNameByQuestionType($questionType);
4065  require_once "Modules/TestQuestionPool/classes/class.{$objectClassName}.php";
4066  }
4067 
4068  $feedbackClassName = self::getFeedbackClassNameByQuestionType($questionType);
4069  require_once "Modules/TestQuestionPool/classes/feedback/class.{$feedbackClassName}.php";
4070  }
4071 
4072  public static function includePluginClass($questionType, $withGuiClass)
4073  {
4074  global $DIC;
4075  $ilPluginAdmin = $DIC['ilPluginAdmin'];
4076 
4077  $classes = array(
4078  self::getObjectClassNameByQuestionType($questionType),
4079  self::getFeedbackClassNameByQuestionType($questionType)
4080  );
4081 
4082  if ($withGuiClass) {
4083  $classes[] = self::getGuiClassNameByQuestionType($questionType);
4084  }
4085 
4086  $pl_names = $ilPluginAdmin->getActivePluginsForSlot(IL_COMP_MODULE, "TestQuestionPool", "qst");
4087  foreach ($pl_names as $pl_name) {
4088  $pl = ilPlugin::getPluginObject(IL_COMP_MODULE, "TestQuestionPool", "qst", $pl_name);
4089  if (strcmp($pl->getQuestionType(), $questionType) == 0) {
4090  foreach ($classes as $class) {
4091  $pl->includeClass("class.{$class}.php");
4092  }
4093 
4094  break;
4095  }
4096  }
4097  }
4098 
4105  public static function _getQuestionTypeName($type_tag)
4106  {
4107  if (file_exists("./Modules/TestQuestionPool/classes/class." . $type_tag . ".php")) {
4108  global $DIC;
4109  $lng = $DIC['lng'];
4110  return $lng->txt($type_tag);
4111  } else {
4112  global $DIC;
4113  $ilPluginAdmin = $DIC['ilPluginAdmin'];
4114  $pl_names = $ilPluginAdmin->getActivePluginsForSlot(IL_COMP_MODULE, "TestQuestionPool", "qst");
4115  foreach ($pl_names as $pl_name) {
4116  $pl = ilPlugin::getPluginObject(IL_COMP_MODULE, "TestQuestionPool", "qst", $pl_name);
4117  if (strcmp($pl->getQuestionType(), $type_tag) == 0) {
4118  return $pl->getQuestionTypeTranslation();
4119  }
4120  }
4121  }
4122  return "";
4123  }
4124 
4134  public static function &_instanciateQuestionGUI($question_id)
4135  {
4136  return self::instantiateQuestionGUI($question_id);
4137  }
4138 
4146  public static function instantiateQuestionGUI($a_question_id)
4147  {
4148  global $DIC;
4149  $ilCtrl = $DIC['ilCtrl'];
4150  $ilDB = $DIC['ilDB'];
4151  $lng = $DIC['lng'];
4152  $ilUser = $DIC['ilUser'];
4153 
4154  if (strcmp($a_question_id, "") != 0) {
4155  $question_type = assQuestion::_getQuestionType($a_question_id);
4156 
4157  assQuestion::_includeClass($question_type, 1);
4158 
4159  $question_type_gui = self::getGuiClassNameByQuestionType($question_type);
4160  $question_gui = new $question_type_gui();
4161  $question_gui->object->loadFromDb($a_question_id);
4162 
4163  $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
4164  $question_gui->object->feedbackOBJ = new $feedbackObjectClassname($question_gui->object, $ilCtrl, $ilDB, $lng);
4165 
4166  $assSettings = new ilSetting('assessment');
4167  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionProcessLockerFactory.php';
4168  $processLockerFactory = new ilAssQuestionProcessLockerFactory($assSettings, $ilDB);
4169  $processLockerFactory->setQuestionId($question_gui->object->getId());
4170  $processLockerFactory->setUserId($ilUser->getId());
4171  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
4172  $processLockerFactory->setAssessmentLogEnabled(ilObjAssessmentFolder::_enabledAssessmentLogging());
4173  $question_gui->object->setProcessLocker($processLockerFactory->getLocker());
4174  } else {
4175  global $DIC;
4176  $ilLog = $DIC['ilLog'];
4177  $ilLog->write('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)', $ilLog->WARNING);
4178  return null;
4179  }
4180  return $question_gui;
4181  }
4182 
4193  public function setExportDetailsXLS($worksheet, $startrow, $active_id, $pass)
4194  {
4195  $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(0) . $startrow, $this->lng->txt($this->getQuestionType()));
4196  $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(1) . $startrow, $this->getTitle());
4197 
4198  return $startrow;
4199  }
4200 
4204  public function __get($value)
4205  {
4206  switch ($value) {
4207  case "id":
4208  return $this->getId();
4209  break;
4210  case "title":
4211  return $this->getTitle();
4212  break;
4213  case "comment":
4214  return $this->getComment();
4215  break;
4216  case "owner":
4217  return $this->getOwner();
4218  break;
4219  case "author":
4220  return $this->getAuthor();
4221  break;
4222  case "question":
4223  return $this->getQuestion();
4224  break;
4225  case "points":
4226  return $this->getPoints();
4227  break;
4228  case "est_working_time":
4229  return $this->getEstimatedWorkingTime();
4230  break;
4231  case "shuffle":
4232  return $this->getShuffle();
4233  break;
4234  case "test_id":
4235  return $this->getTestId();
4236  break;
4237  case "obj_id":
4238  return $this->getObjId();
4239  break;
4240  case "ilias":
4241  return $this->ilias;
4242  break;
4243  case "tpl":
4244  return $this->tpl;
4245  break;
4246  case "page":
4247  return $this->page;
4248  break;
4249  case "outputType":
4250  return $this->getOutputType();
4251  break;
4252  case "suggested_solutions":
4253  return $this->getSuggestedSolutions();
4254  break;
4255  case "original_id":
4256  return $this->getOriginalId();
4257  break;
4258  default:
4259  if (array_key_exists($value, $this->arrData)) {
4260  return $this->arrData[$value];
4261  } else {
4262  return null;
4263  }
4264  break;
4265  }
4266  }
4267 
4271  public function __set($key, $value)
4272  {
4273  switch ($key) {
4274  case "id":
4275  $this->setId($value);
4276  break;
4277  case "title":
4278  $this->setTitle($value);
4279  break;
4280  case "comment":
4281  $this->setComment($value);
4282  break;
4283  case "owner":
4284  $this->setOwner($value);
4285  break;
4286  case "author":
4287  $this->setAuthor($value);
4288  break;
4289  case "question":
4290  $this->setQuestion($value);
4291  break;
4292  case "points":
4293  $this->setPoints($value);
4294  break;
4295  case "est_working_time":
4296  if (is_array($value)) {
4297  $this->setEstimatedWorkingTime($value["h"], $value["m"], $value["s"]);
4298  }
4299  break;
4300  case "shuffle":
4301  $this->setShuffle($value);
4302  break;
4303  case "test_id":
4304  $this->setTestId($value);
4305  break;
4306  case "obj_id":
4307  $this->setObjId($value);
4308  break;
4309  case "outputType":
4310  $this->setOutputType($value);
4311  break;
4312  case "original_id":
4313  $this->setOriginalId($value);
4314  break;
4315  case "page":
4316  $this->page = &$value;
4317  break;
4318  default:
4319  $this->arrData[$key] = $value;
4320  break;
4321  }
4322  }
4323 
4324  public function getNrOfTries()
4325  {
4326  return (int) $this->nr_of_tries;
4327  }
4328 
4329  public function setNrOfTries($a_nr_of_tries)
4330  {
4331  $this->nr_of_tries = $a_nr_of_tries;
4332  }
4333 
4334  public function setExportImagePath($a_path)
4335  {
4336  $this->export_image_path = (string) $a_path;
4337  }
4338 
4339  public static function _questionExistsInTest($question_id, $test_id)
4340  {
4341  global $DIC;
4342  $ilDB = $DIC['ilDB'];
4343 
4344  if ($question_id < 1) {
4345  return false;
4346  }
4347 
4348  $result = $ilDB->queryF(
4349  "SELECT question_fi FROM tst_test_question WHERE question_fi = %s AND test_fi = %s",
4350  array('integer', 'integer'),
4351  array($question_id, $test_id)
4352  );
4353  if ($result->numRows() == 1) {
4354  return true;
4355  } else {
4356  return false;
4357  }
4358  }
4359 
4366  public function formatSAQuestion($a_q)
4367  {
4368  return $this->getSelfAssessmentFormatter()->format($a_q);
4369  }
4370 
4374  protected function getSelfAssessmentFormatter()
4375  {
4376  require_once 'Modules/TestQuestionPool/classes/questions/class.ilAssSelfAssessmentQuestionFormatter.php';
4377  return new \ilAssSelfAssessmentQuestionFormatter();
4378  }
4379 
4380  // scorm2004-start ???
4381 
4387  public function setPreventRteUsage($a_val)
4388  {
4389  $this->prevent_rte_usage = $a_val;
4390  }
4391 
4397  public function getPreventRteUsage()
4398  {
4399  return $this->prevent_rte_usage;
4400  }
4401 
4406  {
4407  $this->lmMigrateQuestionTypeGenericContent($migrator);
4408  $this->lmMigrateQuestionTypeSpecificContent($migrator);
4409  $this->saveToDb();
4410 
4411  $this->feedbackOBJ->migrateContentForLearningModule($migrator, $this->getId());
4412  }
4413 
4418  {
4419  $this->setQuestion($migrator->migrateToLmContent($this->getQuestion()));
4420  }
4421 
4426  {
4427  // overwrite if any question type specific content except feedback needs to be migrated
4428  }
4429 
4435  public function setSelfAssessmentEditingMode($a_selfassessmenteditingmode)
4436  {
4437  $this->selfassessmenteditingmode = $a_selfassessmenteditingmode;
4438  }
4439 
4446  {
4448  }
4449 
4455  public function setDefaultNrOfTries($a_defaultnroftries)
4456  {
4457  $this->defaultnroftries = $a_defaultnroftries;
4458  }
4459 
4465  public function getDefaultNrOfTries()
4466  {
4467  return (int) $this->defaultnroftries;
4468  }
4469 
4470  // scorm2004-end ???
4471 
4477  public static function lookupParentObjId($questionId)
4478  {
4479  global $DIC;
4480  $ilDB = $DIC['ilDB'];
4481 
4482  $query = "SELECT obj_fi FROM qpl_questions WHERE question_id = %s";
4483 
4484  $res = $ilDB->queryF($query, array('integer'), array((int) $questionId));
4485  $row = $ilDB->fetchAssoc($res);
4486 
4487  return $row['obj_fi'];
4488  }
4489 
4500  public static function lookupOriginalParentObjId($originalQuestionId)
4501  {
4502  return self::lookupParentObjId($originalQuestionId);
4503  }
4504 
4505  protected function duplicateQuestionHints($originalQuestionId, $duplicateQuestionId)
4506  {
4507  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintList.php';
4508  $hintIds = ilAssQuestionHintList::duplicateListForQuestion($originalQuestionId, $duplicateQuestionId);
4509 
4511  require_once 'Modules/TestQuestionPool/classes/class.ilAssHintPage.php';
4512 
4513  foreach ($hintIds as $originalHintId => $duplicateHintId) {
4514  $originalPageObject = new ilAssHintPage($originalHintId);
4515  $originalXML = $originalPageObject->getXMLContent();
4516 
4517  $duplicatePageObject = new ilAssHintPage();
4518  $duplicatePageObject->setId($duplicateHintId);
4519  $duplicatePageObject->setParentId($this->getId());
4520  $duplicatePageObject->setXMLContent($originalXML);
4521  $duplicatePageObject->createFromXML();
4522  }
4523  }
4524  }
4525 
4526  protected function duplicateSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId)
4527  {
4528  global $DIC;
4529  $ilDB = $DIC['ilDB'];
4530 
4531  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionSkillAssignmentList.php';
4532  $assignmentList = new ilAssQuestionSkillAssignmentList($ilDB);
4533  $assignmentList->setParentObjId($srcParentId);
4534  $assignmentList->setQuestionIdFilter($srcQuestionId);
4535  $assignmentList->loadFromDb();
4536 
4537  foreach ($assignmentList->getAssignmentsByQuestionId($srcQuestionId) as $assignment) {
4538  /* @var ilAssQuestionSkillAssignment $assignment */
4539 
4540  $assignment->setParentObjId($trgParentId);
4541  $assignment->setQuestionId($trgQuestionId);
4542  $assignment->saveToDb();
4543  }
4544  }
4545 
4546  public function syncSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId)
4547  {
4548  global $DIC;
4549  $ilDB = $DIC['ilDB'];
4550 
4551  require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionSkillAssignmentList.php';
4552  $assignmentList = new ilAssQuestionSkillAssignmentList($ilDB);
4553  $assignmentList->setParentObjId($trgParentId);
4554  $assignmentList->setQuestionIdFilter($trgQuestionId);
4555  $assignmentList->loadFromDb();
4556 
4557  foreach ($assignmentList->getAssignmentsByQuestionId($trgQuestionId) as $assignment) {
4558  /* @var ilAssQuestionSkillAssignment $assignment */
4559 
4560  $assignment->deleteFromDb();
4561  }
4562 
4563  $this->duplicateSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId);
4564  }
4565 
4578  public function isAnswered($active_id, $pass = null)
4579  {
4580  return true;
4581  }
4582 
4595  public static function isObligationPossible($questionId)
4596  {
4597  return false;
4598  }
4599 
4600  public function isAutosaveable()
4601  {
4602  return true;
4603  }
4604 
4617  protected static function getNumExistingSolutionRecords($activeId, $pass, $questionId)
4618  {
4619  global $DIC;
4620  $ilDB = $DIC['ilDB'];
4621 
4622  $query = "
4623  SELECT count(active_fi) cnt
4624 
4625  FROM tst_solutions
4626 
4627  WHERE active_fi = %s
4628  AND question_fi = %s
4629  AND pass = %s
4630  ";
4631 
4632  $res = $ilDB->queryF(
4633  $query,
4634  array('integer','integer','integer'),
4635  array($activeId, $questionId, $pass)
4636  );
4637 
4638  $row = $ilDB->fetchAssoc($res);
4639 
4640  return (int) $row['cnt'];
4641  }
4642 
4650  {
4652  }
4653 
4661  {
4663  require_once 'Modules/TestQuestionPool/exceptions/class.ilTestQuestionPoolException.php';
4664  throw new ilTestQuestionPoolException('invalid additional content editing mode given: ' . $additinalContentEditingMode);
4665  }
4666 
4667  $this->additinalContentEditingMode = $additinalContentEditingMode;
4668  }
4669 
4677  {
4679  }
4680 
4688  public function isValidAdditionalContentEditingMode($additionalContentEditingMode)
4689  {
4690  if (in_array($additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
4691  return true;
4692  }
4693 
4694  return false;
4695  }
4696 
4704  {
4705  return array(
4706  self::ADDITIONAL_CONTENT_EDITING_MODE_RTE,
4707  self::ADDITIONAL_CONTENT_EDITING_MODE_IPE
4708  );
4709  }
4710 
4715  {
4716  $this->questionChangeListeners[] = $listener;
4717  }
4718 
4722  public function getQuestionChangeListeners()
4723  {
4725  }
4726 
4727  private function notifyQuestionCreated()
4728  {
4729  foreach ($this->getQuestionChangeListeners() as $listener) {
4730  $listener->notifyQuestionCreated($this);
4731  }
4732  }
4733 
4734  private function notifyQuestionEdited()
4735  {
4736  foreach ($this->getQuestionChangeListeners() as $listener) {
4737  $listener->notifyQuestionEdited($this);
4738  }
4739  }
4740 
4741  private function notifyQuestionDeleted()
4742  {
4743  foreach ($this->getQuestionChangeListeners() as $listener) {
4744  $listener->notifyQuestionDeleted($this);
4745  }
4746  }
4747 
4752  {
4753  require_once 'Services/Html/classes/class.ilHtmlPurifierFactory.php';
4754  return ilHtmlPurifierFactory::_getInstanceByType('qpl_usersolution');
4755  }
4756 
4758  {
4759  return ilHtmlPurifierFactory::_getInstanceByType('qpl_usersolution');
4760  }
4761 
4762  protected function buildQuestionDataQuery()
4763  {
4764  return "
4765  SELECT qpl_questions.*,
4766  {$this->getAdditionalTableName()}.*
4767  FROM qpl_questions
4768  LEFT JOIN {$this->getAdditionalTableName()}
4769  ON {$this->getAdditionalTableName()}.question_fi = qpl_questions.question_id
4770  WHERE qpl_questions.question_id = %s
4771  ";
4772  }
4773 
4774  public function setLastChange($lastChange)
4775  {
4776  $this->lastChange = $lastChange;
4777  }
4778 
4779  public function getLastChange()
4780  {
4781  return $this->lastChange;
4782  }
4783 
4794  protected function getCurrentSolutionResultSet($active_id, $pass, $authorized = true)
4795  {
4796  global $DIC;
4797  $ilDB = $DIC['ilDB'];
4798 
4799  if ($this->getStep() !== null) {
4800  $query = "
4801  SELECT *
4802  FROM tst_solutions
4803  WHERE active_fi = %s
4804  AND question_fi = %s
4805  AND pass = %s
4806  AND step = %s
4807  AND authorized = %s
4808  ";
4809 
4810  return $ilDB->queryF(
4811  $query,
4812  array('integer', 'integer', 'integer', 'integer', 'integer'),
4813  array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
4814  );
4815  } else {
4816  $query = "
4817  SELECT *
4818  FROM tst_solutions
4819  WHERE active_fi = %s
4820  AND question_fi = %s
4821  AND pass = %s
4822  AND authorized = %s
4823  ";
4824 
4825  return $ilDB->queryF(
4826  $query,
4827  array('integer', 'integer', 'integer', 'integer'),
4828  array($active_id, $this->getId(), $pass, (int) $authorized)
4829  );
4830  }
4831  }
4832 
4839  protected function removeSolutionRecordById($solutionId)
4840  {
4841  global $DIC;
4842  $ilDB = $DIC['ilDB'];
4843 
4844  return $ilDB->manipulateF(
4845  "DELETE FROM tst_solutions WHERE solution_id = %s",
4846  array('integer'),
4847  array($solutionId)
4848  );
4849  }
4850 
4851  // hey: prevPassSolutions - selected file reuse, copy solution records
4858  protected function getSolutionRecordById($solutionId)
4859  {
4860  global $DIC; /* @var ILIAS\DI\Container $DIC */
4861  $ilDB = $DIC['ilDB'];
4862 
4863  $res = $ilDB->queryF(
4864  "SELECT * FROM tst_solutions WHERE solution_id = %s",
4865  array('integer'),
4866  array($solutionId)
4867  );
4868 
4869  while ($row = $ilDB->fetchAssoc($res)) {
4870  return $row;
4871  }
4872  }
4873  // hey.
4874 
4883  public function removeIntermediateSolution($active_id, $pass)
4884  {
4885  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use ($active_id, $pass) {
4886  $this->removeCurrentSolution($active_id, $pass, false);
4887  });
4888  }
4889 
4898  public function removeCurrentSolution($active_id, $pass, $authorized = true)
4899  {
4900  global $DIC;
4901  $ilDB = $DIC['ilDB'];
4902 
4903  if ($this->getStep() !== null) {
4904  $query = "
4905  DELETE FROM tst_solutions
4906  WHERE active_fi = %s
4907  AND question_fi = %s
4908  AND pass = %s
4909  AND step = %s
4910  AND authorized = %s
4911  ";
4912 
4913  return $ilDB->manipulateF(
4914  $query,
4915  array('integer', 'integer', 'integer', 'integer', 'integer'),
4916  array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
4917  );
4918  } else {
4919  $query = "
4920  DELETE FROM tst_solutions
4921  WHERE active_fi = %s
4922  AND question_fi = %s
4923  AND pass = %s
4924  AND authorized = %s
4925  ";
4926 
4927  return $ilDB->manipulateF(
4928  $query,
4929  array('integer', 'integer', 'integer', 'integer'),
4930  array($active_id, $this->getId(), $pass, (int) $authorized)
4931  );
4932  }
4933  }
4934 
4935  // fau: testNav - add timestamp as parameter to saveCurrentSolution
4947  public function saveCurrentSolution($active_id, $pass, $value1, $value2, $authorized = true, $tstamp = null)
4948  {
4949  global $DIC;
4950  $ilDB = $DIC['ilDB'];
4951 
4952  $next_id = $ilDB->nextId("tst_solutions");
4953 
4954  $fieldData = array(
4955  "solution_id" => array("integer", $next_id),
4956  "active_fi" => array("integer", $active_id),
4957  "question_fi" => array("integer", $this->getId()),
4958  "value1" => array("clob", $value1),
4959  "value2" => array("clob", $value2),
4960  "pass" => array("integer", $pass),
4961  "tstamp" => array("integer", isset($tstamp) ? $tstamp : time()),
4962  'authorized' => array('integer', (int) $authorized)
4963  );
4964 
4965  if ($this->getStep() !== null) {
4966  $fieldData['step'] = array("integer", $this->getStep());
4967  }
4968 
4969  return $ilDB->insert("tst_solutions", $fieldData);
4970  }
4971  // fau.
4972 
4983  public function updateCurrentSolution($solutionId, $value1, $value2, $authorized = true)
4984  {
4985  global $DIC;
4986  $ilDB = $DIC['ilDB'];
4987 
4988  $fieldData = array(
4989  "value1" => array("clob", $value1),
4990  "value2" => array("clob", $value2),
4991  "tstamp" => array("integer", time()),
4992  'authorized' => array('integer', (int) $authorized)
4993  );
4994 
4995  if ($this->getStep() !== null) {
4996  $fieldData['step'] = array("integer", $this->getStep());
4997  }
4998 
4999  return $ilDB->update("tst_solutions", $fieldData, array(
5000  'solution_id' => array('integer', $solutionId)
5001  ));
5002  }
5003 
5004  // fau: testNav - added parameter to keep the timestamp (default: false)
5005  public function updateCurrentSolutionsAuthorization($activeId, $pass, $authorized, $keepTime = false)
5006  {
5007  global $DIC;
5008  $ilDB = $DIC['ilDB'];
5009 
5010  $fieldData = array(
5011  'authorized' => array('integer', (int) $authorized)
5012  );
5013 
5014  if (!$keepTime) {
5015  $fieldData['tstamp'] = array('integer', time());
5016  }
5017 
5018  $whereData = array(
5019  'question_fi' => array('integer', $this->getId()),
5020  'active_fi' => array('integer', $activeId),
5021  'pass' => array('integer', $pass)
5022  );
5023 
5024  if ($this->getStep() !== null) {
5025  $whereData['step'] = array("integer", $this->getStep());
5026  }
5027 
5028  return $ilDB->update('tst_solutions', $fieldData, $whereData);
5029  }
5030  // fau.
5031 
5032  // hey: prevPassSolutions - motivation slowly decreases on imagemap
5034  protected static function getKeyValuesImplosionSeparator()
5035  {
5036  return self::KEY_VALUES_IMPLOSION_SEPARATOR;
5037  }
5038  public static function implodeKeyValues($keyValues)
5039  {
5040  return implode(self::getKeyValuesImplosionSeparator(), $keyValues);
5041  }
5042  public static function explodeKeyValues($keyValues)
5043  {
5044  return explode(self::getKeyValuesImplosionSeparator(), $keyValues);
5045  }
5046 
5047  protected function deleteDummySolutionRecord($activeId, $passIndex)
5048  {
5049  foreach ($this->getSolutionValues($activeId, $passIndex, false) as $solutionRec) {
5050  if (0 == strlen($solutionRec['value1']) && 0 == strlen($solutionRec['value2'])) {
5051  $this->removeSolutionRecordById($solutionRec['solution_id']);
5052  }
5053  }
5054  }
5055 
5056  protected function isDummySolutionRecord($solutionRecord)
5057  {
5058  return !strlen($solutionRecord['value1']) && !strlen($solutionRecord['value2']);
5059  }
5060 
5061  protected function deleteSolutionRecordByValues($activeId, $passIndex, $authorized, $matchValues)
5062  {
5063  global $DIC; /* @var ILIAS\DI\Container $DIC */
5064  $ilDB = $DIC['ilDB'];
5065 
5066  $types = array("integer", "integer", "integer", "integer");
5067  $values = array($activeId, $this->getId(), $passIndex, (int) $authorized);
5068  $valuesCondition = array();
5069 
5070  foreach ($matchValues as $valueField => $value) {
5071  switch ($valueField) {
5072  case 'value1':
5073  case 'value2':
5074  $valuesCondition[] = "{$valueField} = %s";
5075  $types[] = 'text';
5076  $values[] = $value;
5077  break;
5078 
5079  default:
5080  require_once 'Modules/TestQuestionPool/exceptions/class.ilTestQuestionPoolException.php';
5081  throw new ilTestQuestionPoolException('invalid value field given: ' . $valueField);
5082  }
5083  }
5084 
5085  $valuesCondition = implode(' AND ', $valuesCondition);
5086 
5087  $query = "
5088  DELETE FROM tst_solutions
5089  WHERE active_fi = %s
5090  AND question_fi = %s
5091  AND pass = %s
5092  AND authorized = %s
5093  AND $valuesCondition
5094  ";
5095 
5096  if ($this->getStep() !== null) {
5097  $query .= " AND step = %s ";
5098  $types[] = 'integer';
5099  $values[] = $this->getStep();
5100  }
5101 
5102  $ilDB->manipulateF($query, $types, $values);
5103  }
5104 
5105  protected function duplicateIntermediateSolutionAuthorized($activeId, $passIndex)
5106  {
5107  foreach ($this->getSolutionValues($activeId, $passIndex, false) as $rec) {
5108  $this->saveCurrentSolution($activeId, $passIndex, $rec['value1'], $rec['value2'], true, $rec['tstamp']);
5109  }
5110  }
5111 
5112  protected function forceExistingIntermediateSolution($activeId, $passIndex, $considerDummyRecordCreation)
5113  {
5114  $intermediateSolution = $this->getSolutionValues($activeId, $passIndex, false);
5115 
5116  if (!count($intermediateSolution)) {
5117  // make the authorized solution intermediate (keeping timestamps)
5118  // this keeps the solution_ids in synch with eventually selected in $_POST['deletefiles']
5119  $this->updateCurrentSolutionsAuthorization($activeId, $passIndex, false, true);
5120 
5121  // create a backup as authorized solution again (keeping timestamps)
5122  $this->duplicateIntermediateSolutionAuthorized($activeId, $passIndex);
5123 
5124  if ($considerDummyRecordCreation) {
5125  // create an additional dummy record to indicate the existence of an intermediate solution
5126  // even if all entries are deleted from the intermediate solution later
5127  $this->saveCurrentSolution($activeId, $passIndex, null, null, false, null);
5128  }
5129  }
5130  }
5131  // hey.
5132 
5136  public static function setResultGateway($resultGateway)
5137  {
5138  self::$resultGateway = $resultGateway;
5139  }
5140 
5144  public static function getResultGateway()
5145  {
5146  return self::$resultGateway;
5147  }
5148 
5152  public function setStep($step)
5153  {
5154  $this->step = $step;
5155  }
5156 
5160  public function getStep()
5161  {
5162  return $this->step;
5163  }
5164 
5170  public static function sumTimesInISO8601FormatH_i_s_Extended($time1, $time2)
5171  {
5174  return gmdate('H:i:s', $time);
5175  }
5176 
5181  public static function convertISO8601FormatH_i_s_ExtendedToSeconds($time)
5182  {
5183  $sec = 0;
5184  $time_array = explode(':', $time);
5185  if (sizeof($time_array) == 3) {
5186  $sec += $time_array[0] * 3600;
5187  $sec += $time_array[1] * 60;
5188  $sec += $time_array[2];
5189  }
5190  return $sec;
5191  }
5192 
5193  public function toJSON()
5194  {
5195  return json_encode(array());
5196  }
5197 
5198  abstract public function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null);
5199 
5200  // hey: prevPassSolutions - check for authorized solution
5201  public function intermediateSolutionExists($active_id, $pass)
5202  {
5203  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
5204  return (bool) $solutionAvailability['intermediate'];
5205  }
5206  public function authorizedSolutionExists($active_id, $pass)
5207  {
5208  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
5209  return (bool) $solutionAvailability['authorized'];
5210  }
5211  public function authorizedOrIntermediateSolutionExists($active_id, $pass)
5212  {
5213  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
5214  return (bool) $solutionAvailability['authorized'] || (bool) $solutionAvailability['intermediate'];
5215  }
5216  // hey.
5217 
5223  protected function lookupMaxStep($active_id, $pass)
5224  {
5226  global $DIC;
5227  $ilDB = $DIC['ilDB'];
5228 
5229  $res = $ilDB->queryF(
5230  "SELECT MAX(step) max_step FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
5231  array("integer", "integer", "integer"),
5232  array($active_id, $pass, $this->getId())
5233  );
5234 
5235  $row = $ilDB->fetchAssoc($res);
5236 
5237  $maxStep = $row['max_step'];
5238 
5239  return $maxStep;
5240  }
5241 
5242  // fau: testNav - new function lookupForExistingSolutions
5249  public function lookupForExistingSolutions($activeId, $pass)
5250  {
5252  global $DIC;
5253  $ilDB = $DIC['ilDB'];
5254 
5255  $return = array(
5256  'authorized' => false,
5257  'intermediate' => false
5258  );
5259 
5260  $query = "
5261  SELECT authorized, COUNT(*) cnt
5262  FROM tst_solutions
5263  WHERE active_fi = %s
5264  AND question_fi = %s
5265  AND pass = %s
5266  ";
5267 
5268  if ($this->getStep() !== null) {
5269  $query .= " AND step = " . $ilDB->quote((int) $this->getStep(), 'integer') . " ";
5270  }
5271 
5272  $query .= "
5273  GROUP BY authorized
5274  ";
5275 
5276  $result = $ilDB->queryF($query, array('integer', 'integer', 'integer'), array($activeId, $this->getId(), $pass));
5277 
5278  while ($row = $ilDB->fetchAssoc($result)) {
5279  if ($row['authorized']) {
5280  $return['authorized'] = $row['cnt'] > 0;
5281  } else {
5282  $return['intermediate'] = $row['cnt'] > 0;
5283  }
5284  }
5285  return $return;
5286  }
5287  // fau.
5288 
5289  public function isAddableAnswerOptionValue($qIndex, $answerOptionValue)
5290  {
5291  return false;
5292  }
5293 
5294  public function addAnswerOptionValue($qIndex, $answerOptionValue, $points)
5295  {
5296  }
5297 
5298  public function removeAllExistingSolutions()
5299  {
5300  global $DIC; /* @var ILIAS\DI\Container $DIC */
5301 
5302  $query = "DELETE FROM tst_solutions WHERE question_fi = %s";
5303 
5304  $DIC->database()->manipulateF($query, array('integer'), array($this->getId()));
5305  }
5306 
5307  public function removeExistingSolutions($activeId, $pass)
5308  {
5309  global $DIC;
5310  $ilDB = $DIC['ilDB'];
5311 
5312  $query = "
5313  DELETE FROM tst_solutions
5314  WHERE active_fi = %s
5315  AND question_fi = %s
5316  AND pass = %s
5317  ";
5318 
5319  if ($this->getStep() !== null) {
5320  $query .= " AND step = " . $ilDB->quote((int) $this->getStep(), 'integer') . " ";
5321  }
5322 
5323  return $ilDB->manipulateF(
5324  $query,
5325  array('integer', 'integer', 'integer'),
5326  array($activeId, $this->getId(), $pass)
5327  );
5328  }
5329 
5330  public function resetUsersAnswer($activeId, $pass)
5331  {
5332  $this->removeExistingSolutions($activeId, $pass);
5333  $this->removeResultRecord($activeId, $pass);
5334 
5335  $this->log($activeId, "log_user_solution_willingly_deleted");
5336 
5337  self::_updateTestPassResults(
5338  $activeId,
5339  $pass,
5341  $this->getProcessLocker(),
5342  $this->getTestId()
5343  );
5344  }
5345 
5346  public function removeResultRecord($activeId, $pass)
5347  {
5348  global $DIC;
5349  $ilDB = $DIC['ilDB'];
5350 
5351  $query = "
5352  DELETE FROM tst_test_result
5353  WHERE active_fi = %s
5354  AND question_fi = %s
5355  AND pass = %s
5356  ";
5357 
5358  if ($this->getStep() !== null) {
5359  $query .= " AND step = " . $ilDB->quote((int) $this->getStep(), 'integer') . " ";
5360  }
5361 
5362  return $ilDB->manipulateF(
5363  $query,
5364  array('integer', 'integer', 'integer'),
5365  array($activeId, $this->getId(), $pass)
5366  );
5367  }
5368 
5369  public static function missingResultRecordExists($activeId, $pass, $questionIds)
5370  {
5371  global $DIC;
5372  $ilDB = $DIC['ilDB'];
5373 
5374  $IN_questionIds = $ilDB->in('question_fi', $questionIds, false, 'integer');
5375 
5376  $query = "
5377  SELECT COUNT(*) cnt
5378  FROM tst_test_result
5379  WHERE active_fi = %s
5380  AND pass = %s
5381  AND $IN_questionIds
5382  ";
5383 
5384  $row = $ilDB->fetchAssoc($ilDB->queryF(
5385  $query,
5386  array('integer', 'integer'),
5387  array($activeId, $pass)
5388  ));
5389 
5390  return $row['cnt'] < count($questionIds);
5391  }
5392 
5393  public static function getQuestionsMissingResultRecord($activeId, $pass, $questionIds)
5394  {
5395  global $DIC;
5396  $ilDB = $DIC['ilDB'];
5397 
5398  $IN_questionIds = $ilDB->in('question_fi', $questionIds, false, 'integer');
5399 
5400  $query = "
5401  SELECT question_fi
5402  FROM tst_test_result
5403  WHERE active_fi = %s
5404  AND pass = %s
5405  AND $IN_questionIds
5406  ";
5407 
5408  $res = $ilDB->queryF(
5409  $query,
5410  array('integer', 'integer'),
5411  array($activeId, $pass)
5412  );
5413 
5414  $questionsHavingResultRecord = array();
5415 
5416  while ($row = $ilDB->fetchAssoc($res)) {
5417  $questionsHavingResultRecord[] = $row['question_fi'];
5418  }
5419 
5420  $questionsMissingResultRecordt = array_diff(
5421  $questionIds,
5422  $questionsHavingResultRecord
5423  );
5424 
5425  return $questionsMissingResultRecordt;
5426  }
5427 
5428  public static function lookupResultRecordExist($activeId, $questionId, $pass)
5429  {
5430  global $DIC;
5431  $ilDB = $DIC['ilDB'];
5432 
5433  $query = "
5434  SELECT COUNT(*) cnt
5435  FROM tst_test_result
5436  WHERE active_fi = %s
5437  AND question_fi = %s
5438  AND pass = %s
5439  ";
5440 
5441  $row = $ilDB->fetchAssoc($ilDB->queryF($query, array('integer', 'integer', 'integer'), array($activeId, $questionId, $pass)));
5442 
5443  return $row['cnt'] > 0;
5444  }
5445 
5450  public function fetchValuePairsFromIndexedValues(array $indexedValues)
5451  {
5452  $valuePairs = array();
5453 
5454  foreach ($indexedValues as $value1 => $value2) {
5455  $valuePairs[] = array('value1' => $value1, 'value2' => $value2);
5456  }
5457 
5458  return $valuePairs;
5459  }
5460 
5465  public function fetchIndexedValuesFromValuePairs(array $valuePairs)
5466  {
5467  $indexedValues = array();
5468 
5469  foreach ($valuePairs as $valuePair) {
5470  $indexedValues[ $valuePair['value1'] ] = $valuePair['value2'];
5471  }
5472 
5473  return $indexedValues;
5474  }
5475 
5480  {
5482  }
5483 
5488  {
5489  $this->obligationsToBeConsidered = $obligationsToBeConsidered;
5490  }
5491 
5492  public function updateTimestamp()
5493  {
5494  global $DIC;
5495  $ilDB = $DIC['ilDB'];
5496 
5497  $ilDB->manipulateF(
5498  "UPDATE qpl_questions SET tstamp = %s WHERE question_id = %s",
5499  array('integer', 'integer'),
5500  array(time(), $this->getId())
5501  );
5502  }
5503 
5504  // fau: testNav - new function getTestQuestionConfig()
5505  // hey: prevPassSolutions - get caching independent from configuration (config once)
5506  // renamed: getTestPresentationConfig() -> does the caching
5507  // completed: extracted instance building
5508  // avoids configuring cached instances on every access
5509  // allows a stable reconfigure of the instance from outside
5514 
5519  public function getTestPresentationConfig()
5520  {
5521  if ($this->testQuestionConfigInstance === null) {
5522  $this->testQuestionConfigInstance = $this->buildTestPresentationConfig();
5523  }
5524 
5526  }
5527 
5536  protected function buildTestPresentationConfig()
5537  {
5538  include_once('Modules/TestQuestionPool/classes/class.ilTestQuestionConfig.php');
5539  return new ilTestQuestionConfig();
5540  }
5541  // hey.
5542  // fau.
5543 
5544  public function savePartial()
5545  {
5546  return false;
5547  }
5548 
5549  /* doubles isInUse? */
5550  public function isInActiveTest() : bool
5551  {
5552  $query = 'SELECT user_fi FROM tst_active ' . PHP_EOL
5553  . 'JOIN tst_test_question ON tst_test_question.test_fi = tst_active.test_fi ' . PHP_EOL
5554  . 'JOIN qpl_questions ON qpl_questions.question_id = tst_test_question.question_fi ' . PHP_EOL
5555  . 'WHERE qpl_questions.obj_fi = ' . $this->db->quote($this->getObjId(), 'integer');
5556 
5557  $res = $this->db->query($query);
5558  return $res->numRows() > 0;
5559  }
5560 }
static _getUserIdFromActiveId($active_id)
const ADDITIONAL_CONTENT_EDITING_MODE_IPE
constant for additional content editing mode "pageobject"
isInUse($question_id="")
Checks whether the question is in use or not.
static isObligationPossible($questionId)
returns boolean wether it is possible to set this question type as obligatory or not considering the ...
static isCoreQuestionType($questionType)
static resetOriginalId($questionId)
static makeDirParents($a_dir)
Create a new directory and all parent directories.
static logAction($logtext="", $active_id="", $question_id="")
Logs an action into the Test&Assessment log.
deletePageOfQuestion($question_id)
Deletes the page object of a question with a given ID.
afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
getId()
Gets the id of the assQuestion object.
static _getManualScoringTypes()
Retrieve the manual scoring settings as type strings.
static prepareFormOutput($a_str, $a_strip=false)
prepares string output for html forms public
saveToDb($original_id="")
Saves the question to the database.
static getListByQuestionId($questionId)
instantiates a question hint list for the passed question id
static _getWorkingTimeOfParticipantForPass($active_id, $pass)
Returns the complete working time in seconds for a test participant.
static isFileAvailable($file)
$export_image_path
(Web) Path to images
Test Question configuration.
static _getMobsOfObject($a_type, $a_id, $a_usage_hist_nr=0, $a_lang="-")
get mobs of object
static _getQuestionText($a_q_id)
Returns question text.
getFlashPathWeb()
Returns the web image path for web accessable flash applications of a question.
static _instanciateQuestion($question_id)
Creates an instance of a question with a given question id.
static getAllowedImageMaterialFileExtensions()
static _getOriginalId($question_id)
Returns the original id of a question.
static getPluginObject(string $a_ctype, string $a_cname, string $a_slot_id, string $a_pname)
formatSAQuestion($a_q)
Format self assessment question.
setSuggestedSolution($solution_id="", $subquestion_index=0, $is_import=false)
Sets a suggested solution for the question.
static getObjectClassNameByQuestionType($questionType)
Taxonomy node <-> item assignment.
migrateContentForLearningModule(ilAssSelfAssessmentMigrator $migrator)
static _includeClass($question_type, $gui=0)
Include the php class file for a given question type.
static _updateTestResultCache($active_id, ilAssQuestionProcessLocker $processLocker=null)
Move this to a proper place.
static getQuestionTypeFromDb($question_id)
get question type for question id
static _isWriteable($object_id, $user_id)
Returns true, if the question pool is writeable by a given user.
forceExistingIntermediateSolution($activeId, $passIndex, $considerDummyRecordCreation)
_getTotalAnswers($a_q_id)
get number of answers for question id (static) note: do not use $this inside this method ...
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static sumTimesInISO8601FormatH_i_s_Extended($time1, $time2)
static _getParticipantData($active_id)
Retrieves a participant name from active id.
_questionExistsInPool($question_id)
Returns true if the question already exists in the database and is assigned to a question pool...
copySuggestedSolutionFiles($source_questionpool_id, $source_question_id)
static getNumExistingSolutionRecords($activeId, $pass, $questionId)
returns the number of existing solution records for the given test active / pass and given question i...
$data
Definition: storeScorm.php:23
const IL_INST_ID
Definition: constants.php:38
duplicateIntermediateSolutionAuthorized($activeId, $passIndex)
buildHashedImageFilename($plain_image_filename, $unique=false)
getSuggestedSolutionPath()
Returns the path for a suggested solution.
getTitleFilenameCompliant()
returns the object title prepared to be used as a filename
$_SESSION["AccountId"]
static _getQuestionType($question_id)
Returns the question type of a question with a given id.
static _getSolutionMaxPass($question_id, $active_id)
Returns the maximum pass a users question solution.
$result
$mobs
Definition: imgupload.php:54
static getUsageOfObject($a_obj_id, $a_include_titles=false)
Get usage of object.
static lookupResultRecordExist($activeId, $questionId, $pass)
getQuestionType()
Returns the question type of the question.
static includeCoreClass($questionType, $withGuiClass)
syncSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId)
static _getTotalRightAnswers($a_q_id)
get number of answers for question id (static) note: do not use $this inside this method ...
getPoints()
Returns the maximum available points for the question.
static originalQuestionExists($questionId)
$type
static _isUsedInRandomTest($question_id="")
Checks whether the question is used in a random test or not.
copyPageOfQuestion($a_q_id)
questionTitleExists($questionpool_id, $title)
Returns TRUE if the question title exists in the database.
toXML($a_include_header=true, $a_include_binary=true, $a_shuffle=false, $test_output=false, $force_image_references=false)
Returns a QTI xml representation of the question.
buildTestPresentationConfig()
build basic test question configuration instance
$_GET["client_id"]
static getQuestionsMissingResultRecord($activeId, $pass, $questionIds)
__set($key, $value)
Object setter.
__get($value)
Object getter.
Abstract basic class which is to be extended by the concrete assessment question type classes...
static _needsManualScoring($question_id)
& _getSuggestedSolution($question_id, $subquestion_index=0)
Returns a suggested solution for a given subquestion index.
Class ChatMainBarProvider .
setDefaultNrOfTries($a_defaultnroftries)
Set Default Nr of Tries.
addQTIMaterial(&$a_xml_writer, $a_material, $close_material_tag=true, $add_mobs=true)
Creates a QTI material tag from a plain text or xhtml text.
static prepareTextareaOutput($txt_output, $prepare_for_latex_output=false, $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free...
static _getIdForImportId($a_import_id)
get current object id for import id (static)
createPageObject()
create page object of question
intermediateSolutionExists($active_id, $pass)
static getDataWebPath(string $relative_path='')
This is originally a fix for https://mantis.ilias.de/view.php?id=35707; in general, the handling of those pathes shold be improved or better, avoided entirely (e.g.
adjustReachedPointsByScoringOptions($points, $active_id, $pass=null)
Adjust the given reached points by checks for all special scoring options in the test container...
ensureNonNegativePoints($points)
deleteAnswers($question_id)
Deletes datasets from answers tables.
deleteDummySolutionRecord($activeId, $passIndex)
calculateResultsFromSolution($active_id, $pass=null, $obligationsEnabled=false)
Calculates the question results from a previously saved question solution.
static & _instanciateQuestionGUI($question_id)
Creates an instance of a question gui with a given question id.
static _areAnswered($a_user_id, $a_question_ids)
Checks if an array of question ids is answered by an user or not.
getSuggestedSolutionTitle($subquestion_index=0)
Returns the title of a suggested solution at a given subquestion_index.
getSolutionValues($active_id, $pass=null, $authorized=true)
Loads solutions of a given user from the database an returns it.
setId($id=-1)
Sets the id of the assQuestion object.
copyXHTMLMediaObjectsOfQuestion($a_q_id)
static _getQuestionTypeName($type_tag)
Return the translation for a given question type tag.
getAdditionalTableName()
Returns the name of the additional question data table in the database.
duplicateSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId)
static isForcePassResultUpdateEnabled()
static _getSuggestedSolutionCount($question_id)
Returns the number of suggested solutions associated with a question.
getUserSolutionPreferingIntermediate($active_id, $pass=null)
getImagePathWeb()
Returns the web image path for web accessable images of a question.
Question page object.
$objId
Definition: xapitoken.php:39
savePreviewData(ilAssQuestionPreviewSession $previewSession)
getSolutionMaxPass($active_id)
Returns the maximum pass a users question solution.
$target_id
Definition: goto.php:51
removeResultRecord($activeId, $pass)
createNewQuestion($a_create_page=true)
Creates a new question without an owner when a new question is created This assures that an ID is giv...
static includePluginClass($questionType, $withGuiClass)
setEstimatedWorkingTime($hour=0, $min=0, $sec=0)
Sets the estimated working time of a question from given hour, minute and second. ...
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
static _lookupTitle($a_id)
lookup object title
syncSuggestedSolutionFiles($original_id)
Syncs the files of a suggested solution if the question is synced.
setNewOriginalId($newId)
beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
fromXML(&$item, &$questionpool_id, &$tst_id, &$tst_object, &$question_counter, &$import_mapping, array $solutionhints=[])
Receives parameters from a QTI parser and creates a valid ILIAS question object.
getAdditionalContentEditingMode()
getter for additional content editing mode for this question
getJavaPath()
Returns the image path for web accessable images of a question.
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
deleteAdditionalTableData($question_id)
Deletes datasets from the additional question table in the database.
isAnswered($active_id, $pass=null)
returns boolean wether the question is answered during test pass or not
setEstimatedWorkingTimeFromDurationString($durationString)
Sets the estimated working time of a question from a given datetime string.
getSelfAssessmentEditingMode()
Get Self-Assessment Editing Mode.
isAddableAnswerOptionValue($qIndex, $answerOptionValue)
setNrOfTries($a_nr_of_tries)
static _removeUsage($a_mob_id, $a_type, $a_id, $a_usage_hist_nr=0, $a_lang="-")
Remove usage of mob in another container.
setAdditionalContentEditingMode($additinalContentEditingMode)
setter for additional content editing mode for this question
static lookupParentObjId($questionId)
ilDBInterface $ilDB
const OUTPUT_JAVASCRIPT
isHTML($a_text)
Checks if a given string contains HTML or not.
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
loadFromDb($question_id)
Loads the question from the database.
persistPreviewState(ilAssQuestionPreviewSession $previewSession)
persists the preview state for current user and question
authorizedSolutionExists($active_id, $pass)
static getASCIIFilename($a_filename)
convert utf8 to ascii filename
setShuffle($shuffle=true)
Sets the shuffle flag.
static _replaceMediaObjectImageSrc($a_text, $a_direction=0, $nic=IL_INST_ID)
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
getObjId()
Get the object id of the container object.
getValidAdditionalContentEditingModes()
getter for valid additional content editing modes
static _getMaximumPoints($question_id)
Returns the maximum points, a learner can reach answering the question.
getShuffle()
Gets the shuffle flag.
$arrData
Associative array to store properties.
static _getAllReferences($a_id)
get all reference ids of object
isValidAdditionalContentEditingMode($additionalContentEditingMode)
returns the fact wether the passed additional content mode is valid or not
& getInstances()
Gets all instances of the question.
fetchIndexedValuesFromValuePairs(array $valuePairs)
$index
Definition: metadata.php:128
setProcessLocker($processLocker)
static _getInternalLinkHref($target="")
isPreviewSolutionCorrect(ilAssQuestionPreviewSession $previewSession)
static _getQuestionInfo($question_id)
Returns question information from the database.
static convertISO8601FormatH_i_s_ExtendedToSeconds($time)
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
static isQuestionObligatory($question_id)
checks wether the question with given id is marked as obligatory or not
static deleteHintsByQuestionIds($questionIds)
Deletes all question hints relating to questions included in given question ids.
getSuggestedSolution($subquestion_index=0)
Returns a suggested solution for a given subquestion index.
authorizedOrIntermediateSolutionExists($active_id, $pass)
removeSolutionRecordById($solutionId)
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession)
duplicateQuestionHints($originalQuestionId, $duplicateQuestionId)
addAnswerOptionValue($qIndex, $answerOptionValue, $points)
if($format !==null) $name
Definition: metadata.php:230
static _getLogLanguage()
retrieve the log language for assessment logging
persistWorkingState($active_id, $pass=null, $obligationsEnabled=false, $authorized=true)
persists the working state for current testactive and testpass
supportsJavascriptOutput()
Returns true if the question type supports JavaScript output.
getJavaPathWeb()
Returns the web image path for web accessable java applets of a question.
getFlashPath()
Returns the image path for web accessable flash files of a question.
static isAllowedImageMimeType($mimeType)
getTestId()
Gets the test id of the assQuestion object.
setAuthor($author="")
Sets the authors name of the assQuestion object.
setObligationsToBeConsidered($obligationsToBeConsidered)
Assessment hint page object.
static _enabledAssessmentLogging()
check wether assessment logging is enabled or not
purifyAndPrepareTextAreaOutput(string $content)
getAuthor()
Gets the authors name of the assQuestion object.
getTotalAnswers()
get total number of answers
getQuestionTypeID()
Returns the question type of the question.
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
static _updateQuestionCount($object_id)
Updates the number of available questions for a question pool in the database.
foreach($_POST as $key=> $value) $res
$keys
Definition: metadata.php:187
static _getQuestionCountAndPointsForPassOfParticipant($active_id, $pass)
static setForcePassResultUpdateEnabled($forcePassResultsUpdateEnabled)
updateCurrentSolutionsAuthorization($activeId, $pass, $authorized, $keepTime=false)
static _isWorkedThrough($active_id, $question_id, $pass=null)
Returns true if the question was worked through in the given pass Worked through means that the user ...
duplicate($for_test=true, $title="", $author="", $owner="", $testObjId=null)
static _addLog($user_id, $object_id, $logtext, $question_id="", $original_id="", $test_only=false, $test_ref_id=null)
Add an assessment log entry.
comment()
Definition: comment.php:2
log($active_id, $langVar)
static _getScoreCutting($active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test...
saveCurrentSolution($active_id, $pass, $value1, $value2, $authorized=true, $tstamp=null)
static getImagePath($img, $module_path="", $mode="output", $offline=false)
get image path (for images located in a template directory)
static _getResultPass($active_id)
Retrieves the pass number that should be counted for a given user.
static moveUploadedFile($a_file, $a_name, $a_target, $a_raise_errors=true, $a_mode="move_uploaded")
move uploaded file
static explodeKeyValues($keyValues)
saveWorkingData($active_id, $pass=null, $authorized=true)
Saves the learners input of the question to the database.
static _lookupObjId($a_id)
setOutputType($outputType=OUTPUT_HTML)
Sets the output type.
global $DIC
Definition: goto.php:24
isComplete()
Returns true, if a question is complete for use.
static _getCountSystem($active_id)
Gets the count system for the calculation of points.
static setResultGateway($resultGateway)
static implodeKeyValues($keyValues)
static $forcePassResultsUpdateEnabled
if(!defined('PATH_SEPARATOR')) $GLOBALS['_PEAR_default_error_mode']
Definition: PEAR.php:64
getQuestion()
Gets the question string of the question object.
getComment()
Gets the comment string of the assQuestion object.
const IL_COMP_MODULE
static lookupOriginalParentObjId($originalQuestionId)
returns the parent object id for given original question id (should be a qpl id, but theoretically it...
redirection script todo: (a better solution should control the processing via a xml file) ...
$xml
Definition: metadata.php:332
static createDirectory($a_dir, $a_mod=0755)
create directory
static saveOriginalId($questionId, $originalId)
Class ilObjMediaObject.
$nr_of_tries
Number of tries.
$query
const CLIENT_WEB_DIR
Definition: constants.php:45
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
getSuggestedSolutionPathWeb()
Returns the web path for a suggested solution.
static _updateObjectiveResult($a_user_id, $a_active_id, $a_question_id)
static signFile($path_to_file)
moveUploadedMediaFile($file, $name)
Move an uploaded media file to an public accessible temp dir to present it.
getDefaultNrOfTries()
Get Default Nr of Tries.
isClone($question_id="")
Checks whether the question is a clone of another question or not.
updateCurrentSolution($solutionId, $value1, $value2, $authorized=true)
static removeTrailingPathSeparators($path)
cleanupMediaObjectUsage()
synchronises appearances of media objects in the question with media object usage table ...
deleteSuggestedSolutions()
Deletes all suggestes solutions in the database.
setPreventRteUsage($a_val)
Set prevent rte usage.
fixSvgToPng($imageFilenameContainingString)
static _instantiateQuestion($question_id)
isDummySolutionRecord($solutionRecord)
setExternalId($external_id)
$filename
Definition: buildRTE.php:89
fetchValuePairsFromIndexedValues(array $indexedValues)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
static duplicateListForQuestion($originalQuestionId, $duplicateQuestionId)
duplicates a hint list from given original question id to given duplicate question id and returns an ...
isAdditionalContentEditingModePageObject()
isser for additional "pageobject" content editing mode
getTestOutputSolutions($activeId, $pass)
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
static $allowedCharsetsByMimeType
static _questionExistsInTest($question_id, $test_id)
setPoints($a_points)
Sets the maximum available points for the question.
saveQuestionDataToDb($original_id="")
_resolveIntLinks($question_id)
static _getSuggestedSolutionOutput($question_id)
Returns the output of the suggested solution.
const KEY_VALUES_IMPLOSION_SEPARATOR
onDuplicate($originalParentId, $originalQuestionId, $duplicateParentId, $duplicateQuestionId)
Will be called when a question is duplicated (inside a question pool or for insertion in a test) ...
_questionExists($question_id)
Returns true if the question already exists in the database.
static $imageSourceFixReplaceMap
getOwner()
Gets the creator/owner ID of the assQuestion object.
static isAllowedImageFileExtension($mimeType, $fileExtension)
lmMigrateQuestionTypeGenericContent(ilAssSelfAssessmentMigrator $migrator)
lookupTestId($active_id)
lookupCurrentTestPass($active_id, $pass)
resetUsersAnswer($activeId, $pass)
getEstimatedWorkingTime()
Gets the estimated working time of a question.
setQuestion($question="")
Sets the question string of the question object.
removeCurrentSolution($active_id, $pass, $authorized=true)
setTestId($id=-1)
Sets the test id of the assQuestion object.
ensureCurrentTestPass($active_id, $pass)
__construct(Container $dic, ilPlugin $plugin)
prepareTextareaOutput($txt_output, $prepare_for_latex_output=false, $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output in tests.
deleteSolutionRecordByValues($activeId, $passIndex, $authorized, $matchValues)
const ADDITIONAL_CONTENT_EDITING_MODE_RTE
constant for additional content editing mode "default"
getTestPresentationConfig()
Get the test question configuration (initialised once)
setExportDetailsXLS($worksheet, $startrow, $active_id, $pass)
Creates an Excel worksheet for the detailed cumulated results of this question.
buildImagePath($questionId, $parentObjectId)
global $ilDB
removeExistingSolutions($activeId, $pass)
setOriginalId($original_id)
static getResultGateway()
static _getInstanceByType(string $type)
Factory method for creating purifier instances.
setLastChange($lastChange)
getCurrentSolutionResultSet($active_id, $pass, $authorized=true)
Get a restulset for the current user solution for a this question by active_id and pass...
getAnswerTableName()
Returns the name of the answer table in the database.
static buildExamId($active_id, $pass, $test_obj_id=null)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
removeIntermediateSolution($active_id, $pass)
getReachedPoints($active_id, $pass=null)
Returns the points, a learner has reached answering the question This is the fast way to get the poin...
$message
Definition: xapiexit.php:14
static getGuiClassNameByQuestionType($questionType)
getTitle()
Gets the title string of the assQuestion object.
static _cleanupMediaObjectUsage($a_text, $a_usage_type, $a_usage_id)
Synchronises appearances of media objects in $a_text with media object usage table.
static setTokenMaxLifetimeInSeconds($token_max_lifetime_in_seconds)
getAdjustedReachedPoints($active_id, $pass=null, $authorizedSolution=true)
returns the reached points ...
const OUTPUT_HTML
addQuestionChangeListener(ilQuestionChangeListener $listener)
static isHTML($a_text)
Checks if a given string contains HTML or not.
static fetchMimeTypeIdentifier($contentTypeString)
$ilUser
Definition: imgupload.php:18
static _exists($a_id, $a_reference=false, $a_type=null)
checks wether a lm content object with specified id exists or not
static _getReachedPoints($active_id, $question_id, $pass=null)
Returns the points, a learner has reached answering the question.
pcArrayShuffle($array)
Shuffles the values of a given array.
duplicateSuggestedSolutionFiles($parent_id, $question_id)
Duplicates the files of a suggested solution if the question is duplicated.
getOutputType()
Gets the output type.
onCopy($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId)
Will be called when a question is copied (into another question pool)
getSuggestedSolutions()
Return the suggested solutions.
static $allowedImageMaterialFileExtensionsByMimeType
_resolveInternalLink($internal_link)
setTitle($title="")
Sets the title string of the assQuestion object.
fixUnavailableSkinImageSources($html)
setObjId($obj_id=0)
Set the object id of the container object.
getActiveUserData($active_id)
Returns the user id and the test id for a given active id.
static delDir($a_dir, $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
static _saveUsage($a_mob_id, $a_type, $a_id, $a_usage_hist_nr=0, $a_lang="-")
Save usage of mob within another container (e.g.
setExportImagePath($a_path)
setComment($comment="")
Sets the comment string of the assQuestion object.
setShuffler(ilArrayElementShuffler $shuffler)
static $allowedFileExtensionsByMimeType
static getFeedbackClassNameByQuestionType($questionType)
static _getQuestionTitle($question_id)
Returns the question title of a question with a given id.
static getKeyValuesImplosionSeparator()
$_POST["username"]
static _setReachedPoints($active_id, $question_id, $points, $maxpoints, $pass, $manualscoring, $obligationsEnabled)
Sets the points, a learner has reached answering the question Additionally objective results are upda...
static _getTitle($a_q_id)
Returns the title of a question.
static instantiateQuestionGUI($a_question_id)
Creates an instance of a question gui with a given question id.
keyInArray($searchkey, $array)
returns TRUE if the key occurs in an array
QTIMaterialToString($a_material)
Reads an QTI material tag an creates a text string.
$i
Definition: metadata.php:24
isNonEmptyItemListPostSubmission($postSubmissionFieldname)
setOwner($owner="")
Sets the creator/owner ID of the assQuestion object.
setSelfAssessmentEditingMode($a_selfassessmenteditingmode)
Set Self-Assessment Editing Mode.
static missingResultRecordExists($activeId, $pass, $questionIds)
getSolutionRecordById($solutionId)
getPreventRteUsage()
Get prevent rte usage.
static _isWriteable($question_id, $user_id)
Returns true if the question is writeable by a certain user.
static getAllowedFileExtensionsForMimeType($mimeType)