ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.ilAssQuestionList.php
Go to the documentation of this file.
1 <?php
2 
20 
28 {
29  private ilDBInterface $db;
30  private ilLanguage $lng;
33 
39  private $parentObjIdsFilter = array();
40 
46  private $parentObjId = null;
47 
53  private $parentObjType = 'qpl';
54 
60  private $availableTaxonomyIds = array();
61 
67  private $fieldFilters = array();
68 
74  private $taxFilters = array();
75 
81  private $taxParentIds = array();
82 
88  private $taxParentTypes = array();
89 
95  private $answerStatusActiveId = null;
96 
100  private $forcedQuestionIds = array();
101 
106  protected $join_obj_data = true;
107 
108 
112  public const QUESTION_ANSWER_STATUS_NON_ANSWERED = 'nonAnswered';
113  public const QUESTION_ANSWER_STATUS_WRONG_ANSWERED = 'wrongAnswered';
114  public const QUESTION_ANSWER_STATUS_CORRECT_ANSWERED = 'correctAnswered';
115 
119  public const ANSWER_STATUS_FILTER_ALL_NON_CORRECT = 'allNonCorrect';
120  public const ANSWER_STATUS_FILTER_NON_ANSWERED_ONLY = 'nonAnswered';
121  public const ANSWER_STATUS_FILTER_WRONG_ANSWERED_ONLY = 'wrongAnswered';
122 
128  private $answerStatusFilter = null;
129 
130  public const QUESTION_INSTANCE_TYPE_ORIGINALS = 'QST_INSTANCE_TYPE_ORIGINALS';
131  public const QUESTION_INSTANCE_TYPE_DUPLICATES = 'QST_INSTANCE_TYPE_DUPLICATES';
132  public const QUESTION_INSTANCE_TYPE_ALL = 'QST_INSTANCE_TYPE_ALL';
133  private $questionInstanceTypeFilter = self::QUESTION_INSTANCE_TYPE_ORIGINALS;
134 
137 
138  public const QUESTION_COMPLETION_STATUS_COMPLETE = 'complete';
139  public const QUESTION_COMPLETION_STATUS_INCOMPLETE = 'incomplete';
140  public const QUESTION_COMPLETION_STATUS_BOTH = 'complete/incomplete';
141  private $questionCompletionStatusFilter = self::QUESTION_COMPLETION_STATUS_BOTH;
142 
148  protected $questions = array();
149 
150  public function __construct(
151  ilDBInterface $db,
152  ilLanguage $lng,
153  Refinery $refinery,
154  ilComponentRepository $component_repository
155  ) {
156  $this->db = $db;
157  $this->lng = $lng;
158  $this->refinery = $refinery;
159  $this->component_repository = $component_repository;
160  }
161 
162  public function getParentObjId(): ?int
163  {
164  return $this->parentObjId;
165  }
166 
167  public function setParentObjId($parentObjId): void
168  {
169  $this->parentObjId = $parentObjId;
170  }
171 
172  public function getParentObjectType(): string
173  {
174  return $this->parentObjType;
175  }
176 
177  public function setParentObjectType($parentObjType): void
178  {
179  $this->parentObjType = $parentObjType;
180  }
181 
185  public function getParentObjIdsFilter(): array
186  {
188  }
189 
194  {
195  $this->parentObjIdsFilter = $parentObjIdsFilter;
196  }
197 
199  {
200  $this->questionInstanceTypeFilter = $questionInstanceTypeFilter;
201  }
202 
204  {
206  }
207 
208  public function setIncludeQuestionIdsFilter($questionIdsFilter): void
209  {
210  $this->includeQuestionIdsFilter = $questionIdsFilter;
211  }
212 
213  public function getIncludeQuestionIdsFilter()
214  {
216  }
217 
218  public function getExcludeQuestionIdsFilter()
219  {
221  }
222 
224  {
225  $this->excludeQuestionIdsFilter = $excludeQuestionIdsFilter;
226  }
227 
228  public function getQuestionCompletionStatusFilter(): string
229  {
231  }
232 
234  {
235  $this->questionCompletionStatusFilter = $questionCompletionStatusFilter;
236  }
237 
238  public function addFieldFilter($fieldName, $fieldValue): void
239  {
240  $this->fieldFilters[$fieldName] = $fieldValue;
241  }
242 
243  public function addTaxonomyFilter($taxId, $taxNodes, $parentObjId, $parentObjType): void
244  {
245  $this->taxFilters[$taxId] = $taxNodes;
246  $this->taxParentIds[$taxId] = $parentObjId;
247  $this->taxParentTypes[$taxId] = $parentObjType;
248  }
249 
251  {
252  $this->availableTaxonomyIds = $availableTaxonomyIds;
253  }
254 
255  public function getAvailableTaxonomyIds(): array
256  {
258  }
259 
261  {
262  $this->answerStatusActiveId = $answerStatusActiveId;
263  }
264 
265  public function getAnswerStatusActiveId(): ?int
266  {
268  }
269 
271  {
272  $this->answerStatusFilter = $answerStatusFilter;
273  }
274 
275  public function getAnswerStatusFilter(): ?string
276  {
278  }
279 
285  public function setJoinObjectData($a_val): void
286  {
287  $this->join_obj_data = $a_val;
288  }
289 
295  public function getJoinObjectData(): bool
296  {
297  return $this->join_obj_data;
298  }
299 
304  {
305  $this->forcedQuestionIds = $forcedQuestionIds;
306  }
307 
311  public function getForcedQuestionIds(): array
312  {
314  }
315 
316  private function getParentObjFilterExpression(): ?string
317  {
318  if ($this->getParentObjId()) {
319  return 'qpl_questions.obj_fi = ' . $this->db->quote($this->getParentObjId(), 'integer');
320  }
321 
322  if (count($this->getParentObjIdsFilter())) {
323  return $this->db->in('qpl_questions.obj_fi', $this->getParentObjIdsFilter(), false, 'integer');
324  }
325 
326  return null;
327  }
328 
329  private function getFieldFilterExpressions(): array
330  {
331  $expressions = array();
332 
333  foreach ($this->fieldFilters as $fieldName => $fieldValue) {
334  switch ($fieldName) {
335  case 'title':
336  case 'description':
337  case 'author':
338  case 'lifecycle':
339 
340  $expressions[] = $this->db->like('qpl_questions.' . $fieldName, 'text', "%%$fieldValue%%");
341  break;
342 
343  case 'type':
344 
345  $expressions[] = "qpl_qst_type.type_tag = {$this->db->quote($fieldValue, 'text')}";
346  break;
347 
348  case 'question_id':
349  if ($fieldValue != "" && !is_array($fieldValue)) {
350  $fieldValue = array($fieldValue);
351  }
352  $expressions[] = $this->db->in("qpl_questions.question_id", $fieldValue, false, "integer");
353  break;
354 
355  case 'parent_title':
356  if ($this->join_obj_data) {
357  $expressions[] = $this->db->like('object_data.title', 'text', "%%$fieldValue%%");
358  }
359  break;
360  }
361  }
362 
363  return $expressions;
364  }
365 
366  private function getTaxonomyFilterExpressions(): array
367  {
368  $expressions = array();
369 
370  require_once 'Services/Taxonomy/classes/class.ilTaxonomyTree.php';
371  require_once 'Services/Taxonomy/classes/class.ilTaxNodeAssignment.php';
372 
373  foreach ($this->taxFilters as $taxId => $taxNodes) {
374  $questionIds = array();
375 
376  $forceBypass = true;
377 
378  foreach ($taxNodes as $taxNode) {
379  $forceBypass = false;
380 
381  $taxItemsByTaxParent = $this->getTaxItems(
382  $this->taxParentTypes[$taxId],
383  $this->taxParentIds[$taxId],
384  $taxId,
385  $taxNode
386  );
387 
388  $taxItemsByParent = $this->getTaxItems(
389  $this->parentObjType,
390  $this->parentObjId,
391  $taxId,
392  $taxNode
393  );
394 
395  $taxItems = array_merge($taxItemsByTaxParent, $taxItemsByParent);
396  foreach ($taxItems as $taxItem) {
397  $questionIds[$taxItem['item_id']] = $taxItem['item_id'];
398  }
399  }
400 
401  if (!$forceBypass) {
402  $expressions[] = $this->db->in('question_id', $questionIds, false, 'integer');
403  }
404  }
405 
406  return $expressions;
407  }
408 
416  protected function getTaxItems($parentType, $parentObjId, $taxId, $taxNode): array
417  {
418  $taxTree = new ilTaxonomyTree($taxId);
419 
420  $taxNodeAssignment = new ilTaxNodeAssignment(
421  $parentType,
422  $parentObjId,
423  'quest',
424  $taxId
425  );
426 
427  $subNodes = $taxTree->getSubTreeIds($taxNode);
428  $subNodes[] = $taxNode;
429 
430  return $taxNodeAssignment->getAssignmentsOfNode($subNodes);
431  }
432 
433  private function getQuestionInstanceTypeFilterExpression(): ?string
434  {
435  switch ($this->getQuestionInstanceTypeFilter()) {
436  case self::QUESTION_INSTANCE_TYPE_ORIGINALS:
437  return 'qpl_questions.original_id IS NULL';
438  case self::QUESTION_INSTANCE_TYPE_DUPLICATES:
439  return 'qpl_questions.original_id IS NOT NULL';
440  case self::QUESTION_INSTANCE_TYPE_ALL:
441  default:
442  return null;
443  }
444 
445  return null;
446  }
447 
448  private function getQuestionIdsFilterExpressions(): array
449  {
450  $expressions = array();
451 
452  if (is_array($this->getIncludeQuestionIdsFilter())) {
453  $expressions[] = $this->db->in(
454  'qpl_questions.question_id',
456  false,
457  'integer'
458  );
459  }
460 
461  if (is_array($this->getExcludeQuestionIdsFilter())) {
462  $IN = $this->db->in(
463  'qpl_questions.question_id',
465  true,
466  'integer'
467  );
468 
469  if ($IN == ' 1=2 ') {
470  $IN = ' 1=1 ';
471  } // required for ILIAS < 5.0
472 
473  $expressions[] = $IN;
474  }
475 
476  return $expressions;
477  }
478 
479  private function getParentObjectIdFilterExpression(): ?string
480  {
481  if ($this->parentObjId) {
482  return "qpl_questions.obj_fi = {$this->db->quote($this->parentObjId, 'integer')}";
483  }
484 
485  return null;
486  }
487 
488  private function getAnswerStatusFilterExpressions(): array
489  {
490  $expressions = array();
491 
492  switch ($this->getAnswerStatusFilter()) {
493  case self::ANSWER_STATUS_FILTER_ALL_NON_CORRECT:
494 
495  $expressions[] = '
496  (tst_test_result.question_fi IS NULL OR tst_test_result.points < qpl_questions.points)
497  ';
498  break;
499 
500  case self::ANSWER_STATUS_FILTER_NON_ANSWERED_ONLY:
501 
502  $expressions[] = 'tst_test_result.question_fi IS NULL';
503  break;
504 
505  case self::ANSWER_STATUS_FILTER_WRONG_ANSWERED_ONLY:
506 
507  $expressions[] = 'tst_test_result.question_fi IS NOT NULL';
508  $expressions[] = 'tst_test_result.points < qpl_questions.points';
509  break;
510  }
511 
512  return $expressions;
513  }
514 
515  private function getTableJoinExpression(): string
516  {
517  $tableJoin = "
518  INNER JOIN qpl_qst_type
519  ON qpl_qst_type.question_type_id = qpl_questions.question_type_fi
520  ";
521 
522  if ($this->join_obj_data) {
523  $tableJoin .= "
524  INNER JOIN object_data
525  ON object_data.obj_id = qpl_questions.obj_fi
526  ";
527  }
528 
529  if ($this->getParentObjectType() === 'tst'
530  && $this->getQuestionInstanceTypeFilter() === self::QUESTION_INSTANCE_TYPE_ALL) {
531  $tableJoin .= "
532  INNER JOIN tst_test_question tstquest
533  ON tstquest.question_fi = qpl_questions.question_id
534  ";
535  }
536 
537  if ($this->getAnswerStatusActiveId()) {
538  $tableJoin .= "
539  LEFT JOIN tst_test_result
540  ON tst_test_result.question_fi = qpl_questions.question_id
541  AND tst_test_result.active_fi = {$this->db->quote($this->getAnswerStatusActiveId(), 'integer')}
542  ";
543  }
544 
545  return $tableJoin;
546  }
547 
548  private function getConditionalFilterExpression(): string
549  {
550  $CONDITIONS = array();
551 
552  if ($this->getQuestionInstanceTypeFilterExpression() !== null) {
553  $CONDITIONS[] = $this->getQuestionInstanceTypeFilterExpression();
554  }
555 
556  if ($this->getParentObjFilterExpression() !== null) {
557  $CONDITIONS[] = $this->getParentObjFilterExpression();
558  }
559 
560  if ($this->getParentObjectIdFilterExpression() !== null) {
561  $CONDITIONS[] = $this->getParentObjectIdFilterExpression();
562  }
563 
564  $CONDITIONS = array_merge(
565  $CONDITIONS,
567  $this->getFieldFilterExpressions(),
570  );
571 
572  $CONDITIONS = implode(' AND ', $CONDITIONS);
573 
574  return strlen($CONDITIONS) ? 'AND ' . $CONDITIONS : '';
575  }
576 
577  private function getSelectFieldsExpression(): string
578  {
579  $selectFields = array(
580  'qpl_questions.*',
581  'qpl_qst_type.type_tag',
582  'qpl_qst_type.plugin',
583  'qpl_qst_type.plugin_name',
584  'qpl_questions.points max_points'
585  );
586 
587  if ($this->join_obj_data) {
588  $selectFields[] = 'object_data.title parent_title';
589  }
590 
591  if ($this->getAnswerStatusActiveId()) {
592  $selectFields[] = 'tst_test_result.points reached_points';
593  $selectFields[] = "CASE
594  WHEN tst_test_result.points IS NULL THEN '" . self::QUESTION_ANSWER_STATUS_NON_ANSWERED . "'
595  WHEN tst_test_result.points < qpl_questions.points THEN '" . self::QUESTION_ANSWER_STATUS_WRONG_ANSWERED . "'
596  ELSE '" . self::QUESTION_ANSWER_STATUS_CORRECT_ANSWERED . "'
597  END question_answer_status
598  ";
599  }
600 
601  $selectFields = implode(",\n\t\t\t\t", $selectFields);
602 
603  return "
604  SELECT {$selectFields}
605  ";
606  }
607 
608  private function buildBasicQuery(): string
609  {
610  return "
611  {$this->getSelectFieldsExpression()}
612 
613  FROM qpl_questions
614 
615  {$this->getTableJoinExpression()}
616 
617  WHERE qpl_questions.tstamp > 0
618  ";
619  }
620 
621  private function buildQuery(): string
622  {
623  $query = $this->buildBasicQuery() . "
624  {$this->getConditionalFilterExpression()}
625  ";
626 
627  if (count($this->getForcedQuestionIds())) {
628  $query .= "
629  UNION {$this->buildBasicQuery()}
630  AND {$this->db->in('qpl_questions.question_id', $this->getForcedQuestionIds(), false, 'integer')}
631  ";
632  }
633 
634  return $query;
635  }
636 
637  public function load(): void
638  {
639  $this->checkFilters();
640 
641  $tags_trafo = $this->refinery->string()->stripTags();
642 
643  $query = $this->buildQuery();
644 
645  $res = $this->db->query($query);
646 
647  while ($row = $this->db->fetchAssoc($res)) {
649 
650  if (!$this->isActiveQuestionType($row)) {
651  continue;
652  }
653 
654  $row['title'] = $tags_trafo->transform($row['title'] ?? '&nbsp;');
655  $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
656  $row['author'] = $tags_trafo->transform($row['author']);
657  $row['taxonomies'] = $this->loadTaxonomyAssignmentData($row['obj_fi'], $row['question_id']);
658  $row['ttype'] = $this->lng->txt($row['type_tag']);
659 
660  $this->questions[ $row['question_id'] ] = $row;
661  }
662  }
663 
664  private function loadTaxonomyAssignmentData($parentObjId, $questionId): array
665  {
666  $taxAssignmentData = array();
667 
668  foreach ($this->getAvailableTaxonomyIds() as $taxId) {
669  require_once 'Services/Taxonomy/classes/class.ilTaxonomyTree.php';
670  require_once 'Services/Taxonomy/classes/class.ilTaxNodeAssignment.php';
671 
672  $taxTree = new ilTaxonomyTree($taxId);
673 
674  $taxAssignment = new ilTaxNodeAssignment('qpl', $parentObjId, 'quest', $taxId);
675 
676  $assignments = $taxAssignment->getAssignmentsOfItem($questionId);
677 
678  foreach ($assignments as $assData) {
679  if (!isset($taxAssignmentData[ $assData['tax_id'] ])) {
680  $taxAssignmentData[ $assData['tax_id'] ] = array();
681  }
682 
683  $nodeData = $taxTree->getNodeData($assData['node_id']);
684 
685  $assData['node_lft'] = $nodeData['lft'];
686 
687  $taxAssignmentData[ $assData['tax_id'] ][ $assData['node_id'] ] = $assData;
688  }
689  }
690 
691  return $taxAssignmentData;
692  }
693 
694  private function isActiveQuestionType(array $questionData): bool
695  {
696  if (!isset($questionData['plugin'])) {
697  return false;
698  }
699 
700  if (!$questionData['plugin']) {
701  return true;
702  }
703 
704  if (!$this->component_repository->getComponentByTypeAndName(
706  'TestQuestionPool'
707  )->getPluginSlotById('qst')->hasPluginName($questionData['plugin_name'])) {
708  return false;
709  }
710 
711  return $this->component_repository
712  ->getComponentByTypeAndName(
714  'TestQuestionPool'
715  )
716  ->getPluginSlotById(
717  'qst'
718  )
719  ->getPluginByName(
720  $questionData['plugin_name']
721  )->isActive();
722  }
723 
724  public function getDataArrayForQuestionId($questionId)
725  {
726  return $this->questions[$questionId];
727  }
728 
729  public function getQuestionDataArray(): array
730  {
731  return $this->questions;
732  }
733 
734  public function isInList($questionId): bool
735  {
736  return isset($this->questions[$questionId]);
737  }
738 
748  public function getTitle(string $a_comp_id, string $a_item_type, int $a_item_id): string
749  {
750  if ($a_comp_id != 'qpl' || $a_item_type != 'quest' || !$a_item_id) {
751  return '';
752  }
753 
754  if (!isset($this->questions[$a_item_id])) {
755  return '';
756  }
757 
758  return $this->questions[$a_item_id]['title'];
759  }
760 
761  private function checkFilters(): void
762  {
763  if (strlen($this->getAnswerStatusFilter()) && !$this->getAnswerStatusActiveId()) {
764  require_once 'Modules/TestQuestionPool/exceptions/class.ilTestQuestionPoolException.php';
765 
766  throw new ilTestQuestionPoolException(
767  'No active id given! You cannot use the answer status filter without giving an active id.'
768  );
769  }
770  }
771 }
$res
Definition: ltiservices.php:69
Readable part of repository interface to ilComponentDataDB.
setAnswerStatusFilter($answerStatusFilter)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
isActiveQuestionType(array $questionData)
getTaxItems($parentType, $parentObjId, $taxId, $taxNode)
setExcludeQuestionIdsFilter($excludeQuestionIdsFilter)
getTitle(string $a_comp_id, string $a_item_type, int $a_item_id)
Get title of an assigned item.
getDataArrayForQuestionId($questionId)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setAnswerStatusActiveId($answerStatusActiveId)
static completeMissingPluginName($questionTypeData)
setIncludeQuestionIdsFilter($questionIdsFilter)
setParentObjectType($parentObjType)
__construct(ilDBInterface $db, ilLanguage $lng, Refinery $refinery, ilComponentRepository $component_repository)
addTaxonomyFilter($taxId, $taxNodes, $parentObjId, $parentObjType)
$query
getJoinObjectData()
Get if object data table should be joined.
setQuestionInstanceTypeFilter($questionInstanceTypeFilter)
const ANSWER_STATUS_FILTER_ALL_NON_CORRECT
answer status filter value domain
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setForcedQuestionIds($forcedQuestionIds)
setJoinObjectData($a_val)
Set if object data table should be joined.
addFieldFilter($fieldName, $fieldValue)
setParentObjIdsFilter($parentObjIdsFilter)
loadTaxonomyAssignmentData($parentObjId, $questionId)
const QUESTION_ANSWER_STATUS_NON_ANSWERED
answer status domain for single questions
setQuestionCompletionStatusFilter($questionCompletionStatusFilter)
ilComponentRepository $component_repository
setAvailableTaxonomyIds($availableTaxonomyIds)