ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.ilObjQuestionPool.php
Go to the documentation of this file.
1 <?php
2 
19 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
20 
31 {
34 
35  private array $mob_ids;
36  private array $file_ids;
37  private ?bool $show_taxonomies = null;
38  private bool $skill_service_enabled;
39  private \ILIAS\TestQuestionPool\QuestionInfoService $questioninfo;
40 
47  public function __construct($a_id = 0, $a_call_by_reference = true)
48  {
49  global $DIC;
50  $this->component_repository = $DIC['component.repository'];
51  $this->benchmark = $DIC['ilBench'];
52  $this->questioninfo = $DIC->testQuestionPool()->questionInfo();
53  $this->type = 'qpl';
54  parent::__construct($a_id, $a_call_by_reference);
55 
56  $this->skill_service_enabled = false;
57  }
58 
62  public function create($a_upload = false): int
63  {
64  $id = parent::create();
65 
66  // meta data will be created by
67  // import parser
68  if (!$a_upload) {
69  $this->createMetaData();
70  }
71  return $id;
72  }
73 
80  public function createReference(): int
81  {
82  $result = parent::createReference();
83  $this->saveToDb();
84  return $result;
85  }
86 
93  public function update(): bool
94  {
95  $this->updateMetaData();
96  if (!parent::update()) {
97  return false;
98  }
99 
100  // put here object specific stuff
101 
102  return true;
103  }
104 
110  public function read($a_force_db = false): void
111  {
112  parent::read($a_force_db);
113  $this->loadFromDb();
114  }
115 
122  public function delete(): bool
123  {
124  // always call parent delete function first!!
125  if (!parent::delete()) {
126  return false;
127  }
128 
129  // delete meta data
130  $this->deleteMetaData();
131 
132  //put here your module specific stuff
133  $this->deleteQuestionpool();
134 
135  $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getId());
136  $qsaImportFails->deleteRegisteredImportFails();
137 
138  return true;
139  }
140 
141  public function deleteQuestionpool(): void
142  {
143  $questions = &$this->getAllQuestions();
144 
145  if (count($questions)) {
146  foreach ($questions as $question_id) {
147  $this->deleteQuestion($question_id);
148  }
149  }
150 
151  $qpl_data_dir = ilFileUtils::getDataDir() . '/qpl_data';
152  $directory = $qpl_data_dir . '/qpl_' . $this->getId();
153  if (is_dir($directory)) {
154  ilFileUtils::delDir($directory);
155  }
156  }
157 
158  public function deleteQuestion(int $question_id): void
159  {
160  $question = assQuestion::instantiateQuestion($question_id);
161  $question->delete($question_id);
162  }
163 
164  public function loadFromDb(): void
165  {
166  $result = $this->db->queryF(
167  'SELECT * FROM qpl_questionpool WHERE obj_fi = %s',
168  ['integer'],
169  [$this->getId()]
170  );
171  if ($result->numRows() == 1) {
172  $row = $this->db->fetchAssoc($result);
173  $this->setShowTaxonomies($row['show_taxonomies']);
174  $this->setSkillServiceEnabled($row['skill_service']);
175  }
176  }
177 
178  public function saveToDb(): void
179  {
180  $result = $this->db->queryF(
181  'SELECT id_questionpool FROM qpl_questionpool WHERE obj_fi = %s',
182  ['integer'],
183  [$this->getId()]
184  );
185 
186  if ($result->numRows() == 1) {
187  $result = $this->db->update(
188  'qpl_questionpool',
189  [
190  'show_taxonomies' => ['integer', (int) $this->getShowTaxonomies()],
191  'skill_service' => ['integer', (int) $this->isSkillServiceEnabled()],
192  'tstamp' => ['integer', time()]
193  ],
194  [
195  'obj_fi' => ['integer', $this->getId()]
196  ]
197  );
198  } else {
199  $next_id = $this->db->nextId('qpl_questionpool');
200 
201  $result = $this->db->insert('qpl_questionpool', [
202  'id_questionpool' => ['integer', $next_id],
203  'show_taxonomies' => ['integer', (int) $this->getShowTaxonomies()],
204  'skill_service' => ['integer', (int) $this->isSkillServiceEnabled()],
205  'tstamp' => ['integer', time()],
206  'obj_fi' => ['integer', $this->getId()]
207  ]);
208  }
209  }
210 
211  public function getQuestiontype($question_id)
212  {
213  if ($question_id < 1) {
214  return null;
215  }
216 
217  $result = $this->db->queryF(
218  'SELECT qpl_qst_type.type_tag FROM qpl_questions, qpl_qst_type WHERE qpl_questions.question_type_fi = qpl_qst_type.question_type_id AND qpl_questions.question_id = %s',
219  ['integer'],
220  [$question_id]
221  );
222 
223  if ($result->numRows() == 1) {
224  $data = $this->db->fetchAssoc($result);
225  return $data['type_tag'];
226  }
227  return null;
228  }
229 
230  public function isInUse(int $question_id): bool
231  {
232  $result = $this->db->queryF(
233  'SELECT COUNT(solution_id) solution_count FROM tst_solutions WHERE question_fi = %s',
234  ['integer'],
235  [$question_id]
236  );
237  $row = $this->db->fetchAssoc($result);
238  return $row['solution_count'];
239  }
240 
241  public function createQuestion(string $question_type, int $question_id = -1)
242  {
243  if ($question_id > 0) {
244  return assQuestion::instantiateQuestionGUI($question_id);
245  }
246  $question_type_gui = $question_type . 'GUI';
247  $question_gui = new $question_type_gui();
248  return $question_gui;
249  }
250 
251  public function duplicateQuestion(int $question_id): int
252  {
253  $question = $this->createQuestion('', $question_id);
254  $newtitle = $question->object->getTitle();
255  if ($this->questioninfo->questionTitleExistsInPool($this->getId(), $question->object->getTitle())) {
256  $counter = 2;
257  while ($this->questioninfo->questionTitleExistsInPool(
258  $this->getId(),
259  $question->object->getTitle() . ' (' . $counter . ')'
260  )) {
261  $counter++;
262  }
263  $newtitle = $question->object->getTitle() . ' (' . $counter . ')';
264  }
265  $new_id = $question->object->duplicate(false, $newtitle);
267  return $new_id;
268  }
269 
270  public function copyQuestion(int $question_id, int $questionpool_to): int
271  {
272  $question_gui = $this->createQuestion('', $question_id);
273  if ($question_gui->object->getObjId() == $questionpool_to) {
274  // the question is copied into the same question pool
275  return $this->duplicateQuestion($question_id);
276  } else {
277  // the question is copied into another question pool
278  $newtitle = $question_gui->object->getTitle();
279  if ($this->questioninfo->questionTitleExistsInPool($this->getId(), $question_gui->object->getTitle())) {
280  $counter = 2;
281  while ($this->questioninfo->questionTitleExistsInPool(
282  $this->getId(),
283  $question_gui->object->getTitle() . ' (' . $counter . ')'
284  )) {
285  $counter++;
286  }
287  $newtitle = $question_gui->object->getTitle() . ' (' . $counter . ')';
288  }
289  return $question_gui->object->copyObject($this->getId(), $newtitle);
290  }
291  }
292 
293  public function getPrintviewQuestions(): array
294  {
295  $query_result = $this->db->queryF(
296  'SELECT qpl_questions.*, qpl_qst_type.type_tag, qpl_qst_type.plugin, qpl_questions.tstamp updated FROM qpl_questions, qpl_qst_type WHERE qpl_questions.original_id IS NULL AND qpl_questions.tstamp > 0 AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id AND qpl_questions.obj_fi = %s',
297  ['integer'],
298  [$this->getId()]
299  );
300  $rows = [];
301  $types = $this->getQuestionTypeTranslations();
302  if ($query_result->numRows()) {
303  while ($row = $this->db->fetchAssoc($query_result)) {
304  $row['ttype'] = $types[$row['type_tag']];
305  if ($row['plugin']) {
306  if ($this->isPluginActive($row['type_tag'])) {
307  array_push($rows, $row);
308  }
309  } else {
310  array_push($rows, $row);
311  }
312  }
313  }
314  return $rows;
315  }
316 
320  private function exportXMLSettings($xmlWriter): void
321  {
322  $xmlWriter->xmlStartTag('Settings');
323 
324  $xmlWriter->xmlElement('ShowTaxonomies', null, (int) $this->getShowTaxonomies());
325  $xmlWriter->xmlElement('SkillService', null, (int) $this->isSkillServiceEnabled());
326 
327  $xmlWriter->xmlEndTag('Settings');
328  }
329 
336  public function objectToXmlWriter(ilXmlWriter &$a_xml_writer, $a_inst, $a_target_dir, &$expLog, $questions): void
337  {
338  $ilBench = $this->benchmark;
339 
340  $this->mob_ids = [];
341  $this->file_ids = [];
342 
343  $attrs = [];
344  $attrs['Type'] = 'Questionpool_Test';
345  $a_xml_writer->xmlStartTag('ContentObject', $attrs);
346 
347  // MetaData
348  $this->exportXMLMetaData($a_xml_writer);
349 
350  // Settings
351  $this->exportXMLSettings($a_xml_writer);
352 
353  // PageObjects
354  $expLog->write(date('[y-m-d H:i:s] ') . 'Start Export Page Objects');
355  $ilBench->start('ContentObjectExport', 'exportPageObjects');
356  $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog, $questions);
357  $ilBench->stop('ContentObjectExport', 'exportPageObjects');
358  $expLog->write(date('[y-m-d H:i:s] ') . 'Finished Export Page Objects');
359 
360  // MediaObjects
361  $expLog->write(date('[y-m-d H:i:s] ') . 'Start Export Media Objects');
362  $ilBench->start('ContentObjectExport', 'exportMediaObjects');
363  $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog);
364  $ilBench->stop('ContentObjectExport', 'exportMediaObjects');
365  $expLog->write(date('[y-m-d H:i:s] ') . 'Finished Export Media Objects');
366 
367  // FileItems
368  $expLog->write(date('[y-m-d H:i:s] ') . 'Start Export File Items');
369  $ilBench->start('ContentObjectExport', 'exportFileItems');
370  $this->exportFileItems($a_target_dir, $expLog);
371  $ilBench->stop('ContentObjectExport', 'exportFileItems');
372  $expLog->write(date('[y-m-d H:i:s] ') . 'Finished Export File Items');
373 
374  // skill assignments
375  $this->populateQuestionSkillAssignmentsXml($a_xml_writer, $questions);
376 
377  $a_xml_writer->xmlEndTag('ContentObject');
378  }
379 
384  protected function populateQuestionSkillAssignmentsXml(ilXmlWriter &$a_xml_writer, $questions): void
385  {
386  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
387  $assignmentList->setParentObjId($this->getId());
388  $assignmentList->loadFromDb();
389  $assignmentList->loadAdditionalSkillData();
390 
391  $skillQuestionAssignmentExporter = new ilAssQuestionSkillAssignmentExporter();
392  $skillQuestionAssignmentExporter->setXmlWriter($a_xml_writer);
393  $skillQuestionAssignmentExporter->setQuestionIds($questions);
394  $skillQuestionAssignmentExporter->setAssignmentList($assignmentList);
395  $skillQuestionAssignmentExporter->export();
396  }
397 
404  public function exportXMLMetaData(&$a_xml_writer): void
405  {
406  $md2xml = new ilMD2XML($this->getId(), 0, $this->getType());
407  $md2xml->setExportMode(true);
408  $md2xml->startExport();
409  $a_xml_writer->appendXML($md2xml->getXML());
410  }
411 
412  public function modifyExportIdentifier($a_tag, $a_param, $a_value)
413  {
414  if ($a_tag == 'Identifier' && $a_param == 'Entry') {
415  $a_value = ilUtil::insertInstIntoID($a_value);
416  }
417 
418  return $a_value;
419  }
420 
427  public function exportXMLPageObjects(&$a_xml_writer, $a_inst, &$expLog, $questions): void
428  {
429  $ilBench = $this->benchmark;
430 
431  foreach ($questions as $question_id) {
432  $ilBench->start('ContentObjectExport', 'exportPageObject');
433  $expLog->write(date('[y-m-d H:i:s] ') . 'Page Object ' . $question_id);
434 
435  $attrs = [];
436  $a_xml_writer->xmlStartTag('PageObject', $attrs);
437 
438  // export xml to writer object
439  $ilBench->start('ContentObjectExport', 'exportPageObject_XML');
440  $page_object = new ilAssQuestionPage($question_id);
441  $page_object->buildDom();
442  $page_object->insertInstIntoIDs($a_inst);
443  $mob_ids = $page_object->collectMediaObjects(false);
444  $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
445  $xml = $page_object->getXMLFromDom(false, false, false, '', true);
446  $xml = str_replace('&', '&amp;', $xml);
447  $a_xml_writer->appendXML($xml);
448  $page_object->freeDom();
449  unset($page_object);
450  $ilBench->stop('ContentObjectExport', 'exportPageObject_XML');
451 
452  $ilBench->start("ContentObjectExport", "exportPageObject_CollectMedia");
453  foreach ($mob_ids as $mob_id) {
454  $this->mob_ids[$mob_id] = $mob_id;
455  }
456  $ilBench->stop('ContentObjectExport', 'exportPageObject_CollectMedia');
457 
458  // collect all file items
459  $ilBench->start('ContentObjectExport', 'exportPageObject_CollectFileItems');
460  //$file_ids = $page_obj->getFileItemIds();
461  foreach ($file_ids as $file_id) {
462  $this->file_ids[$file_id] = $file_id;
463  }
464  $ilBench->stop('ContentObjectExport', 'exportPageObject_CollectFileItems');
465 
466  $a_xml_writer->xmlEndTag("PageObject");
467 
468  $ilBench->stop('ContentObjectExport', 'exportPageObject');
469  }
470  }
471 
472  public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void
473  {
474  foreach ($this->mob_ids as $mob_id) {
475  $expLog->write(date('[y-m-d H:i:s] ') . 'Media Object ' . $mob_id);
476  if (ilObjMediaObject::_exists($mob_id)) {
477  $media_obj = new ilObjMediaObject($mob_id);
478  $media_obj->exportXML($a_xml_writer, $a_inst);
479  $media_obj->exportFiles($a_target_dir);
480  unset($media_obj);
481  }
482  }
483  }
484 
489  public function exportFileItems($target_dir, &$expLog): void
490  {
491  foreach ($this->file_ids as $file_id) {
492  $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
493  $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
494  ilFileUtils::makeDir($file_dir);
495  $file_obj = new ilObjFile($file_id, false);
496  $source_file = $file_obj->getFile($file_obj->getVersion());
497  if (!is_file($source_file)) {
498  $source_file = $file_obj->getFile();
499  }
500  if (is_file($source_file)) {
501  copy($source_file, $file_dir . '/' . $file_obj->getFileName());
502  }
503  unset($file_obj);
504  }
505  }
506 
512  public function createExportDirectory(): void
513  {
514  $qpl_data_dir = ilFileUtils::getDataDir() . '/qpl_data';
515  ilFileUtils::makeDir($qpl_data_dir);
516  if (!is_writable($qpl_data_dir)) {
517  $this->error->raiseError(
518  'Questionpool Data Directory (' . $qpl_data_dir
519  . ') not writeable.',
520  $this->error->FATAL
521  );
522  }
523 
524  // create learning module directory (data_dir/lm_data/lm_<id>)
525  $qpl_dir = $qpl_data_dir . '/qpl_' . $this->getId();
526  ilFileUtils::makeDir($qpl_dir);
527  if (!@is_dir($qpl_dir)) {
528  $this->error->raiseError('Creation of Questionpool Directory failed.', $this->error->FATAL);
529  }
530  // create Export subdirectory (data_dir/lm_data/lm_<id>/Export)
532  if (!@is_dir($this->getExportDirectory('xlsx'))) {
533  $this->error->raiseError('Creation of Export Directory failed.', $this->error->FATAL);
534  }
536  if (!@is_dir($this->getExportDirectory('zip'))) {
537  $this->error->raiseError('Creation of Export Directory failed.', $this->error->FATAL);
538  }
539  }
540 
544  public function getExportDirectory($type = ''): string
545  {
546  switch ($type) {
547  case 'xml':
548  $export_dir = ilExport::_getExportDirectory($this->getId(), $type, $this->getType());
549  break;
550  case 'xlsx':
551  case 'zip':
552  $export_dir = ilFileUtils::getDataDir() . "/qpl_data/qpl_{$this->getId()}/export_{$type}";
553  break;
554  default:
555  $export_dir = ilFileUtils::getDataDir() . '/qpl_data' . '/qpl_' . $this->getId() . '/export';
556  break;
557  }
558  return $export_dir;
559  }
560 
561  public static function _setImportDirectory($a_import_dir = null): void
562  {
563  if ($a_import_dir !== null) {
564  ilSession::set('qpl_import_dir', $a_import_dir);
565  return;
566  }
567 
568  ilSession::clear('qpl_import_dir');
569  }
570 
574  public static function _getImportDirectory(): string
575  {
576  return ilSession::get('qpl_import_dir') ?? '';
577  }
578 
579  public function getImportDirectory()
580  {
582  }
583 
589  public function &getAllQuestions(): array
590  {
591  $result = $this->db->queryF(
592  'SELECT question_id FROM qpl_questions WHERE obj_fi = %s AND qpl_questions.tstamp > 0 AND original_id IS NULL',
593  ['integer'],
594  [$this->getId()]
595  );
596  $questions = [];
597  while ($row = $this->db->fetchAssoc($result)) {
598  array_push($questions, $row['question_id']);
599  }
600  return $questions;
601  }
602 
603  public function &getAllQuestionIds(): array
604  {
605  $query_result = $this->db->queryF(
606  'SELECT question_id, qpl_qst_type.type_tag, qpl_qst_type.plugin FROM qpl_questions, qpl_qst_type WHERE original_id IS NULL AND qpl_questions.tstamp > 0 AND obj_fi = %s AND complete = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id',
607  ['integer', 'text'],
608  [$this->getId(), 1]
609  );
610  $questions = [];
611  if ($query_result->numRows()) {
612  while ($row = $this->db->fetchAssoc($query_result)) {
613  if ($row['plugin']) {
614  if ($this->isPluginActive($row['type_tag'])) {
615  array_push($questions, $row['question_id']);
616  }
617  } else {
618  array_push($questions, $row['question_id']);
619  }
620  }
621  }
622  return $questions;
623  }
624 
625  public function checkQuestionParent(int $question_id): bool
626  {
627  $row = $this->db->fetchAssoc(
628  $this->db->queryF(
629  'SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s',
630  ['integer', 'integer'],
631  [$question_id, $this->getId()]
632  )
633  );
634 
635  return (bool) $row['cnt'];
636  }
637 
642  public function getImportMapping(): array
643  {
644  return [];
645  }
646 
654  public function questionsToXML($questions): string
655  {
656  $xml = '';
657  // export button was pressed
658  if (count($questions) > 0) {
659  foreach ($questions as $key => $value) {
660  $question = $this->createQuestion('', $value);
661  $xml .= $question->object->toXML();
662  }
663  if (count($questions) > 1) {
664  $xml = preg_replace('/<\/questestinterop>\s*<.xml.*?>\s*<questestinterop>/', '', $xml);
665  }
666  }
667  $xml = preg_replace(
668  '/(<\?xml[^>]*?>)/',
669  '\\1' . '<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv1p2p1.dtd">',
670  $xml
671  );
672  return $xml;
673  }
674 
675  protected static function _getQuestionCount(int $pool_id): int
676  {
677  global $DIC;
678  $ilDB = $DIC['ilDB'];
679  $result = $ilDB->queryF(
680  'SELECT COUNT(question_id) question_count FROM qpl_questions WHERE obj_fi = %s AND qpl_questions.tstamp > 0 AND original_id IS NULL AND complete = %s',
681  ['integer', 'text'],
682  [$pool_id, 1]
683  );
684  $row = $ilDB->fetchAssoc($result);
685  return $row['question_count'];
686  }
687 
688  public function setShowTaxonomies($show_taxonomies): void
689  {
690  $this->show_taxonomies = $show_taxonomies;
691  }
692 
693  public function getShowTaxonomies(): ?bool
694  {
695  return $this->show_taxonomies;
696  }
697 
704  public static function _hasEqualPoints($a_obj_id, $is_reference = false): int
705  {
706  global $DIC;
707  $ilDB = $DIC['ilDB'];
708 
709  if ($is_reference) {
710  $result = $ilDB->queryF(
711  'SELECT count(DISTINCT qpl_questions.points) equal_points FROM qpl_questions, object_reference WHERE object_reference.ref_id = %s AND qpl_questions.tstamp > 0 AND object_reference.obj_id = qpl_questions.obj_fi AND qpl_questions.original_id IS NULL',
712  ['integer'],
713  [$a_obj_id]
714  );
715  } else {
716  $result = $ilDB->queryF(
717  'SELECT count(DISTINCT points) equal_points FROM qpl_questions WHERE obj_fi = %s AND qpl_questions.tstamp > 0 AND qpl_questions.original_id IS NULL',
718  ['integer'],
719  [$a_obj_id]
720  );
721  }
722  if ($result->numRows() == 1) {
723  $row = $ilDB->fetchAssoc($result);
724  if ($row['equal_points'] == 1) {
725  return 1;
726  } else {
727  return 0;
728  }
729  }
730  return 0;
731  }
732 
738  public function pasteFromClipboard(): bool
739  {
740  $success = false;
741  if (ilSession::get('qpl_clipboard') != null) {
742  $success = true;
743  foreach (ilSession::get('qpl_clipboard') as $question_object) {
744  if (strcmp($question_object['action'], 'move') == 0) {
745  $result = $this->db->queryF(
746  'SELECT obj_fi FROM qpl_questions WHERE question_id = %s',
747  ['integer'],
748  [$question_object['question_id']]
749  );
750  if ($result->numRows() == 1) {
751  $row = $this->db->fetchAssoc($result);
752  $source_questionpool = $row['obj_fi'];
753  $affectedRows = $this->db->manipulateF(
754  'UPDATE qpl_questions SET obj_fi = %s WHERE question_id = %s',
755  ['integer', 'integer'],
756  [$this->getId(), $question_object['question_id']]
757  );
758  if (!$affectedRows) {
759  $success = false;
760  }
761 
762  $source_path = CLIENT_WEB_DIR . '/assessment/' . $source_questionpool . '/' . $question_object['question_id'] . '/';
763  if (@is_dir($source_path)) {
764  $target_path = CLIENT_WEB_DIR . '/assessment/' . $this->getId() . '/';
765  if (!@is_dir($target_path)) {
766  ilFileUtils::makeDirParents($target_path);
767  }
768  rename($source_path, $target_path . $question_object['question_id']);
769  }
770 
771  ilObjQuestionPool::_updateQuestionCount($source_questionpool);
772  }
773  } else {
774  $new_question_id = $this->copyQuestion($question_object['question_id'], $this->getId());
775  if (!$new_question_id) {
776  $success = false;
777  }
778  }
779  }
780  }
781  // update question count of question pool
783  ilSession::clear('qpl_clipboard');
784 
785  return $success;
786  }
787 
794  public function copyToClipboard($question_id): void
795  {
796  if (ilSession::get('qpl_clipboard') == null) {
797  ilSession::set('qpl_clipboard', []);
798  }
799  $clip = ilSession::get('qpl_clipboard');
800  $clip[$question_id] = ['question_id' => $question_id, 'action' => 'copy'];
801  ilSession::set('qpl_clipboard', $clip);
802  }
803 
810  public function moveToClipboard($question_id): void
811  {
812  if (ilSession::get('qpl_clipboard') == null) {
813  ilSession::set('qpl_clipboard', []);
814  }
815  $clip = ilSession::get('qpl_clipboard');
816  $clip[$question_id] = ['question_id' => $question_id, 'action' => 'move'];
817  ilSession::set('qpl_clipboard', $clip);
818  }
819 
820  public function cleanupClipboard($deletedQuestionId): void
821  {
822  if (ilSession::get('qpl_clipboard') == null) {
823  return;
824  }
825 
826  $clip = ilSession::get('qpl_clipboard');
827  if (!isset($clip[$deletedQuestionId])) {
828  return;
829  }
830 
831  unset($clip[$deletedQuestionId]);
832 
833  if (!count($clip)) {
834  ilSession::clear('qpl_clipboard');
835  } else {
836  ilSession::set('qpl_clipboard', $clip);
837  }
838  }
839 
847  public static function _isWriteable($object_id, $user_id): bool
848  {
849  global $DIC;
850  $rbacsystem = $DIC['rbacsystem'];
851 
852  $refs = ilObject::_getAllReferences($object_id);
853  if (count($refs)) {
854  foreach ($refs as $ref_id) {
855  if ($rbacsystem->checkAccess('write', $ref_id) && (ilObject::_hasUntrashedReference($object_id))) {
856  return true;
857  }
858  }
859  }
860  return false;
861  }
862 
870  public function getQuestionDetails($question_ids): array
871  {
872  $result = [];
873  $query_result = $this->db->query(
874  'SELECT qpl_questions.*, qpl_qst_type.type_tag FROM qpl_questions, qpl_qst_type WHERE qpl_questions.question_type_fi = qpl_qst_type.question_type_id AND ' . $ilDB->in(
875  'qpl_questions.question_id',
876  $question_ids,
877  false,
878  'integer'
879  ) . ' ORDER BY qpl_questions.title'
880  );
881  if ($query_result->numRows()) {
882  while ($row = $this->db->fetchAssoc($query_result)) {
883  array_push($result, $row);
884  }
885  }
886  return $result;
887  }
888 
897  public function getDeleteableQuestionDetails($question_ids): array
898  {
899  $result = [];
900  $query_result = $this->db->query(
901  'SELECT qpl_questions.*, qpl_qst_type.type_tag FROM qpl_questions, qpl_qst_type WHERE qpl_questions.question_type_fi = qpl_qst_type.question_type_id AND '
902  . $this->db->in('qpl_questions.question_id', $question_ids, false, 'integer')
903  . ' ORDER BY qpl_questions.title'
904  );
905  if ($query_result->numRows()) {
906  while ($row = $this->db->fetchAssoc($query_result)) {
907  if (!$this->questioninfo->isUsedInRandomTest($row['question_id'])) {
908  array_push($result, $row);
909  } else {
910  // the question was used in a random test prior to ILIAS 3.7 so it was inserted
911  // as a reference to the original question pool object and not as a copy. To allow
912  // the deletion of the question pool object, a copy must be created and all database references
913  // of the original question must changed with the reference of the copy
914 
915  // 1. Create a copy of the original question
916  $question = $this->createQuestion('', $row['question_id']);
917  $duplicate_id = $question->object->duplicate(true);
918  if ($duplicate_id > 0) {
919  // 2. replace the question id in the solutions
920  $affectedRows = $this->db->manipulateF(
921  'UPDATE tst_solutions SET question_fi = %s WHERE question_fi = %s',
922  ['integer', 'integer'],
923  [$duplicate_id, $row['question_id']]
924  );
925 
926  // 3. replace the question id in the question list of random tests
927  $affectedRows = $this->db->manipulateF(
928  'UPDATE tst_test_rnd_qst SET question_fi = %s WHERE question_fi = %s',
929  ['integer', 'integer'],
930  [$duplicate_id, $row['question_id']]
931  );
932 
933  // 4. replace the question id in the test results
934  $affectedRows = $this->db->manipulateF(
935  'UPDATE tst_test_result SET question_fi = %s WHERE question_fi = %s',
936  ['integer', 'integer'],
937  [$duplicate_id, $row['question_id']]
938  );
939 
940  // 5. replace the question id in the test&assessment log
941  $affectedRows = $this->db->manipulateF(
942  'UPDATE ass_log SET question_fi = %s WHERE question_fi = %s',
943  ['integer', 'integer'],
944  [$duplicate_id, $row['question_id']]
945  );
946 
947  // 6. The original question can be deleted, so add it to the list of questions
948  array_push($result, $row);
949  }
950  }
951  }
952  }
953  return $result;
954  }
955 
962  public static function _getAvailableQuestionpools(
963  $use_object_id = false,
964  $equal_points = false,
965  $could_be_offline = false,
966  $showPath = false,
967  $with_questioncount = false,
968  $permission = 'read',
969  $usr_id = ''
970  ): array {
971  global $DIC;
972  $ilUser = $DIC['ilUser'];
973  $ilDB = $DIC['ilDB'];
974  $lng = $DIC['lng'];
975 
976  $result_array = [];
977  $permission = (strlen($permission) == 0) ? 'read' : $permission;
978  $qpls = ilUtil::_getObjectsByOperations('qpl', $permission, (strlen($usr_id)) ? $usr_id : $ilUser->getId(), -1);
979  $obj_ids = [];
980  foreach ($qpls as $ref_id) {
981  $obj_id = ilObject::_lookupObjId($ref_id);
982  $obj_ids[$ref_id] = $obj_id;
983  }
984  $titles = ilObject::_prepareCloneSelection($qpls, 'qpl');
985  if (count($obj_ids)) {
986  $in = $ilDB->in('object_data.obj_id', $obj_ids, false, 'integer');
987  if ($could_be_offline) {
988  $result = $ilDB->query(
989  'SELECT qpl_questionpool.*, object_data.title FROM qpl_questionpool, object_data WHERE ' .
990  'qpl_questionpool.obj_fi = object_data.obj_id AND ' . $in . ' ORDER BY object_data.title'
991  );
992  } else {
993  $result = $ilDB->queryF(
994  'SELECT qpl_questionpool.*, object_data.title FROM qpl_questionpool, object_data WHERE ' .
995  'qpl_questionpool.obj_fi = object_data.obj_id AND ' . $in . ' AND object_data.offline = %s ' .
996  'ORDER BY object_data.title',
997  ['text'],
998  [0]
999  );
1000  }
1001  while ($row = $ilDB->fetchAssoc($result)) {
1002  $add = true;
1003  if ($equal_points) {
1004  if (!ilObjQuestionPool::_hasEqualPoints($row['obj_fi'])) {
1005  $add = false;
1006  }
1007  }
1008  if ($add) {
1009  $ref_id = array_search($row['obj_fi'], $obj_ids);
1010  $title = (($showPath) ? $titles[$ref_id] : $row['title']);
1011  if ($with_questioncount) {
1012  $title .= ' [' . $row['questioncount'] . ' ' . ($row['questioncount'] == 1 ? $lng->txt(
1013  'ass_question'
1014  ) : $lng->txt('assQuestions')) . ']';
1015  }
1016 
1017  if ($use_object_id) {
1018  $result_array[$row['obj_fi']] = [
1019  'qpl_id' => $row['obj_fi'],
1020  'qpl_title' => $row['title'],
1021  'title' => $title,
1022  'count' => $row['questioncount']
1023  ];
1024  } else {
1025  $result_array[$ref_id] = [
1026  'qpl_id' => $row['obj_fi'],
1027  'qpl_title' => $row['title'],
1028  'title' => $title,
1029  'count' => $row['questioncount']
1030  ];
1031  }
1032  }
1033  }
1034  }
1035  return $result_array;
1036  }
1037 
1038  public function getQplQuestions(): array
1039  {
1040  $questions = [];
1041  $result = $this->db->queryF(
1042  'SELECT qpl_questions.question_id FROM qpl_questions WHERE qpl_questions.original_id IS NULL AND qpl_questions.tstamp > 0 AND qpl_questions.obj_fi = %s',
1043  ['integer'],
1044  [$this->getId()]
1045  );
1046  while ($row = $this->db->fetchAssoc($result)) {
1047  array_push($questions, $row['question_id']);
1048  }
1049  return $questions;
1050  }
1051 
1057  public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
1058  {
1059  $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
1060 
1061  $new_obj->update();
1062 
1063  $new_obj->setSkillServiceEnabled($this->isSkillServiceEnabled());
1064  $new_obj->setShowTaxonomies($this->getShowTaxonomies());
1065  $new_obj->saveToDb();
1066 
1067  // clone the questions in the question pool
1068  $questions = $this->getQplQuestions();
1069  $questionIdsMap = [];
1070  foreach ($questions as $question_id) {
1071  $newQuestionId = $new_obj->copyQuestion($question_id, $new_obj->getId());
1072  $questionIdsMap[$question_id] = $newQuestionId;
1073  }
1074 
1075  $md = new ilMD($this->getId(), 0, $this->getType());
1076  $md->cloneMD($new_obj->getId(), 0, $new_obj->getType());
1077  $new_obj->updateMetaData();
1078 
1079  $duplicator = new ilQuestionPoolTaxonomiesDuplicator();
1080  $duplicator->setSourceObjId($this->getId());
1081  $duplicator->setSourceObjType($this->getType());
1082  $duplicator->setTargetObjId($new_obj->getId());
1083  $duplicator->setTargetObjType($new_obj->getType());
1084  $duplicator->setQuestionIdMapping($questionIdsMap);
1085  $duplicator->duplicate($duplicator->getAllTaxonomiesForSourceObject());
1086 
1087  $new_obj->saveToDb();
1088 
1089  return $new_obj;
1090  }
1091 
1092  public function getQuestionTypes($all_tags = false, $fixOrder = false, $withDeprecatedTypes = true): array
1093  {
1094  return self::_getQuestionTypes($all_tags, $fixOrder, $withDeprecatedTypes);
1095  }
1096 
1097  public static function _getQuestionTypes($all_tags = false, $fixOrder = false, $withDeprecatedTypes = true): array
1098  {
1099  global $DIC;
1100  $ilDB = $DIC['ilDB'];
1101  $lng = $DIC['lng'];
1102  $component_factory = $DIC['component.factory'];
1103 
1105  $lng->loadLanguageModule('assessment');
1106  $result = $ilDB->query('SELECT * FROM qpl_qst_type');
1107  $types = [];
1108  while ($row = $ilDB->fetchAssoc($result)) {
1109  if ($all_tags || (!in_array($row['question_type_id'], $forbidden_types))) {
1110  if ($row['plugin'] == 0) {
1111  $types[$lng->txt($row['type_tag'])] = $row;
1112  } else {
1113  foreach ($component_factory->getActivePluginsInSlot('qst') as $pl) {
1114  if (strcmp($pl->getQuestionType(), $row['type_tag']) == 0) {
1115  $types[$pl->getQuestionTypeTranslation()] = $row;
1116  }
1117  }
1118  }
1119  }
1120  }
1121 
1123  $orderer = new ilAssQuestionTypeOrderer($types, $orderMode);
1124  $types = $orderer->getOrderedTypes($withDeprecatedTypes);
1125 
1126  return $types;
1127  }
1128 
1129  public static function getQuestionTypeByTypeId($type_id)
1130  {
1131  global $DIC;
1132  $ilDB = $DIC['ilDB'];
1133 
1134  $query = 'SELECT type_tag FROM qpl_qst_type WHERE question_type_id = %s';
1135  $types = ['integer'];
1136  $values = [$type_id];
1137  $result = $ilDB->queryF($query, $types, $values);
1138 
1139  if ($row = $ilDB->fetchAssoc($result)) {
1140  return $row['type_tag'];
1141  }
1142  return null;
1143  }
1144 
1145  public static function getQuestionTypeTranslations(): array
1146  {
1147  global $DIC;
1148  $ilDB = $DIC['ilDB'];
1149  $lng = $DIC['lng'];
1150  $ilLog = $DIC['ilLog'];
1151  $component_factory = $DIC['component.factory'];
1152 
1153  $lng->loadLanguageModule('assessment');
1154  $result = $ilDB->query('SELECT * FROM qpl_qst_type');
1155  $types = [];
1156  while ($row = $ilDB->fetchAssoc($result)) {
1157  if ($row['plugin'] == 0) {
1158  $types[$row['type_tag']] = $lng->txt($row['type_tag']);
1159  } else {
1160  foreach ($component_factory->getActivePluginsInSlot('qst') as $pl) {
1161  if (strcmp($pl->getQuestionType(), $row['type_tag']) == 0) {
1162  $types[$row['type_tag']] = $pl->getQuestionTypeTranslation();
1163  }
1164  }
1165  }
1166  }
1167  ksort($types);
1168  return $types;
1169  }
1170 
1174  public static function &_getSelfAssessmentQuestionTypes($all_tags = false): array
1175  {
1176  $allowed_types = [
1177  'assSingleChoice' => 1,
1178  'assMultipleChoice' => 2,
1179  'assKprimChoice' => 3,
1180  'assClozeTest' => 4,
1181  'assMatchingQuestion' => 5,
1182  'assOrderingQuestion' => 6,
1183  'assOrderingHorizontal' => 7,
1184  'assImagemapQuestion' => 8,
1185  'assTextSubset' => 9,
1186  'assErrorText' => 10,
1187  'assLongMenu' => 11
1188  ];
1189  $satypes = [];
1190  $qtypes = ilObjQuestionPool::_getQuestionTypes($all_tags);
1191  foreach ($qtypes as $k => $t) {
1192  if (isset($allowed_types[$t['type_tag']])) {
1193  $t['order'] = $allowed_types[$t['type_tag']];
1194  $satypes[$k] = $t;
1195  }
1196  }
1197  return $satypes;
1198  }
1199 
1200  public function getQuestionList(): array
1201  {
1202  $questions = [];
1203  $result = $this->db->queryF(
1204  'SELECT qpl_questions.*, qpl_qst_type.* FROM qpl_questions, qpl_qst_type WHERE qpl_questions.original_id IS NULL AND qpl_questions.obj_fi = %s AND qpl_questions.tstamp > 0 AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id',
1205  ['integer'],
1206  [$this->getId()]
1207  );
1208  while ($row = $this->db->fetchAssoc($result)) {
1209  array_push($questions, $row);
1210  }
1211  return $questions;
1212  }
1213 
1214  public static function _updateQuestionCount(int $object_id): void
1215  {
1216  global $DIC;
1217  $ilDB = $DIC['ilDB'];
1218  $ilDB->manipulateF(
1219  'UPDATE qpl_questionpool SET questioncount = %s, tstamp = %s WHERE obj_fi = %s',
1220  ['integer', 'integer', 'integer'],
1221  [ilObjQuestionPool::_getQuestionCount($object_id), time(), $object_id]
1222  );
1223  }
1224 
1231  public function isPluginActive($questionType): bool
1232  {
1233  if (!$this->component_repository->getComponentByTypeAndName(
1235  'TestQuestionPool'
1236  )->getPluginSlotById('qst')->hasPluginName($questionType)) {
1237  return false;
1238  }
1239 
1240  return $this->component_repository
1241  ->getComponentByTypeAndName(
1243  'TestQuestionPool'
1244  )
1245  ->getPluginSlotById(
1246  'qst'
1247  )
1248  ->getPluginByName(
1249  $questionType
1250  )->isActive();
1251  }
1252 
1253  public function purgeQuestions(): void
1254  {
1255  $incompleteQuestionPurger = new ilAssIncompleteQuestionPurger($this->db);
1256  $incompleteQuestionPurger->setOwnerId($this->user->getId());
1257  $incompleteQuestionPurger->purge();
1258  }
1259 
1265  public function getTaxonomyIds(): array
1266  {
1267  return ilObjTaxonomy::getUsageOfObject($this->getId());
1268  }
1269 
1270  public function isSkillServiceEnabled(): bool
1271  {
1273  }
1274 
1275  public function setSkillServiceEnabled(bool $skill_service_enabled): void
1276  {
1277  $this->skill_service_enabled = $skill_service_enabled;
1278  }
1279 
1281 
1282  public static function isSkillManagementGloballyActivated(): ?bool
1283  {
1284  if (self::$isSkillManagementGloballyActivated === null) {
1285  $skmgSet = new ilSkillManagementSettings();
1286 
1287  self::$isSkillManagementGloballyActivated = $skmgSet->isActivated();
1288  }
1289 
1290  return self::$isSkillManagementGloballyActivated;
1291  }
1292 
1293  public function fromXML($xmlFile): void
1294  {
1295  $parser = new ilObjQuestionPoolXMLParser($this, $xmlFile);
1296  $parser->startParsing();
1297  }
1298 }
string $title
static get(string $a_var)
const ORDER_MODE_FIX
order mode with fixed priority for ordering
getDeleteableQuestionDetails($question_ids)
Returns an array containing the qpl_question and qpl_qst_type fields of deleteable questions for an a...
checkQuestionParent(int $question_id)
exportXMLMetaData(&$a_xml_writer)
export content objects meta data to xml (see ilias_co.dtd)
Readable part of repository interface to ilComponentDataDB.
string $type
exportXMLPageObjects(&$a_xml_writer, $a_inst, &$expLog, $questions)
export page objects to xml (see ilias_co.dtd)
ILIAS TestQuestionPool QuestionInfoService $questioninfo
static _isWriteable($object_id, $user_id)
Returns true, if the question pool is writeable by a given user.
populateQuestionSkillAssignmentsXml(ilXmlWriter &$a_xml_writer, $questions)
const ORDER_MODE_ALPHA
order mode that orders by alphanumerical priority
const IL_INST_ID
Definition: constants.php:40
moveToClipboard($question_id)
Moves a question to the clipboard.
exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
txt(string $a_topic, string $a_default_lang_fallback_mod="")
gets the text for a given topic if the topic is not in the list, the topic itself with "-" will be re...
exportFileItems($target_dir, &$expLog)
export files of file itmes
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getAvailableQuestionpools( $use_object_id=false, $equal_points=false, $could_be_offline=false, $showPath=false, $with_questioncount=false, $permission='read', $usr_id='')
Returns the available question pools for the active user.
pasteFromClipboard()
Copies/Moves a question from the clipboard.
objectToXmlWriter(ilXmlWriter &$a_xml_writer, $a_inst, $a_target_dir, &$expLog, $questions)
export pages of test to xml (see ilias_co.dtd)
static getQuestionTypeByTypeId($type_id)
static _getAllReferences(int $id)
get all reference ids for object ID
createReference()
Creates a database reference id for the object (saves the object to the database and creates a refere...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _hasUntrashedReference(int $obj_id)
checks whether an object has at least one reference that is not in trash
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getUsageOfObject(int $a_obj_id, bool $a_include_titles=false)
setSkillServiceEnabled(bool $skill_service_enabled)
loadLanguageModule(string $a_module)
Load language module.
getImportMapping()
get array of (two) new created questions for import id
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
start(string $a_module, string $a_bench)
start measurement
static _getExportDirectory(int $a_obj_id, string $a_type="xml", string $a_obj_type="", string $a_entity="")
Get export directory for an repository object.
deleteQuestion(int $question_id)
getExportDirectory($type='')
get export directory of questionpool
cloneObject(int $target_id, int $copy_id=0, bool $omit_tree=false)
Creates a 1:1 copy of the object and places the copy in a given repository.
static _lookupObjId(int $ref_id)
static instantiateQuestionGUI(int $a_question_id)
xmlEndTag(string $tag)
Writes an endtag.
global $DIC
Definition: feed.php:28
modifyExportIdentifier($a_tag, $a_param, $a_value)
static _hasEqualPoints($a_obj_id, $is_reference=false)
Checks a question pool for questions with the same maximum points.
getQuestionTypes($all_tags=false, $fixOrder=false, $withDeprecatedTypes=true)
static collectFileItems(ilPageObject $a_page, DOMDocument $a_domdoc)
Get all file items that are used within the page.
static instantiateQuestion(int $question_id)
static & _getSelfAssessmentQuestionTypes($all_tags=false)
setShowTaxonomies($show_taxonomies)
questionsToXML($questions)
Returns a QTI xml representation of a list of questions.
__construct(VocabulariesInterface $vocabularies)
createQuestion(string $question_type, int $question_id=-1)
copyQuestion(int $question_id, int $questionpool_to)
static _prepareCloneSelection(array $ref_ids, string $new_type, bool $show_path=true)
Prepare copy wizard object selection.
ilLanguage $lng
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
__construct($a_id=0, $a_call_by_reference=true)
Constructor public.
static _getForbiddenQuestionTypes()
Returns the forbidden questiontypes for ILIAS.
Class ilObjFile.
cleanupClipboard($deletedQuestionId)
string $key
Consumer key/client ID value.
Definition: System.php:193
static _setImportDirectory($a_import_dir=null)
update()
update object data
createExportDirectory()
creates data directory for export files (data_dir/qpl_data/qpl_<id>/export, depending on data directo...
const CLIENT_WEB_DIR
Definition: constants.php:47
static _getObjectsByOperations( $a_obj_type, string $a_operation, int $a_usr_id=0, int $limit=0)
Get all objects of a specific type and check access This function is not recursive, instead it parses the serialized rbac_pa entries.
copyToClipboard($question_id)
Copies a question to the clipboard.
static getDataDir()
get data directory (outside webspace)
read($a_force_db=false)
read object data from db into object
static _exists(int $id, bool $reference=false, ?string $type=null)
static _updateQuestionCount(int $object_id)
isPluginActive($questionType)
Checks wheather or not a question plugin with a given name is active.
& getAllQuestions()
Retrieve an array containing all question ids of the questionpool.
static _getQuestionCount(int $pool_id)
getQuestionDetails($question_ids)
Returns an array containing the qpl_question and qpl_qst_type fields for an array of question ids...
getTaxonomyIds()
get ids of all taxonomies corresponding to current pool
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
xmlStartTag(string $tag, ?array $attrs=null, bool $empty=false, bool $encode=true, bool $escape=true)
Writes a starttag.
Class ilBenchmark.
static _getImportDirectory()
get import directory of lm
ilComponentRepository $component_repository
static insertInstIntoID(string $a_value)
inserts installation id into ILIAS id
duplicateQuestion(int $question_id)
static clear(string $a_var)
static set(string $a_var, $a_val)
Set a value.
create($a_upload=false)
create questionpool object
static makeDir(string $a_dir)
creates a new directory and inherits all filesystem permissions of the parent directory You may pass ...
static _getQuestionTypes($all_tags=false, $fixOrder=false, $withDeprecatedTypes=true)