ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
QuestionTable.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
33 
34 class QuestionTable extends ilAssQuestionList implements Table\DataRetrieval
35 {
36  public function __construct(
37  protected UIFactory $ui_factory,
38  protected UIRenderer $ui_renderer,
39  protected DataFactory $data_factory,
40  protected Refinery $refinery,
41  protected URLBuilder $url_builder,
42  protected URLBuilderToken $action_parameter_token,
43  protected URLBuilderToken $row_id_token,
44  protected ilDBInterface $db,
45  protected ilLanguage $lng,
46  protected ilComponentRepository $component_repository,
47  protected ilRbacSystem $rbac,
48  protected ?TaxonomyService $taxonomy,
49  protected NotesService $notes_service,
50  protected int $parent_obj_id,
51  protected int $request_ref_id
52  ) {
53  $lng->loadLanguageModule('qpl');
54  parent::__construct($db, $lng, $refinery, $component_repository, $notes_service);
55  if ($this->taxonomy) {
56  $this->setAvailableTaxonomyIds($taxonomy->getUsageOfObject($parent_obj_id));
57  }
58  }
59 
60  public function getTable(): Table\Data
61  {
62  return $this->ui_factory->table()->data(
63  $this->lng->txt('questions'),
64  $this->getColums(),
65  $this
66  )
67  ->withActions($this->getActions())
68  ->withId('qpt' . $this->parent_obj_id . '_' . $this->request_ref_id);
69  }
70 
75  public function getFilter(ilUIService $ui_service, string $action): Filter
76  {
77  $lifecycle_options = array_merge(
78  ['' => $this->lng->txt('qst_lifecycle_filter_all')],
79  ilAssQuestionLifecycle::getDraftInstance()->getSelectOptions($this->lng)
80  );
81  $question_type_options = [
82  '' => $this->lng->txt('filter_all_question_types')
83  ];
84  $question_types = ilObjQuestionPool::_getQuestionTypes();
85  foreach ($question_types as $translation => $row) {
86  $question_type_options[$row['type_tag']] = $translation;
87  }
88 
89  $field_factory = $this->ui_factory->input()->field();
90  $filter_inputs = [
91  'title' => $field_factory->text($this->lng->txt("title")),
92  'description' => $field_factory->text($this->lng->txt("description")),
93  'author' => $field_factory->text($this->lng->txt("author")),
94  'lifecycle' => $field_factory->select($this->lng->txt("qst_lifecycle"), $lifecycle_options),
95  'type' => $field_factory->select($this->lng->txt("type"), $question_type_options),
96  'commented' => $field_factory->select(
97  $this->lng->txt("ass_comments"),
98  [
99  ilAssQuestionList::QUESTION_COMMENTED_ONLY => $this->lng->txt('qpl_filter_commented_only'),
100  ilAssQuestionList::QUESTION_COMMENTED_EXCLUDED => $this->lng->txt('qpl_filter_commented_exclude')
101  ]
102  )
103  ];
104 
105  if ($this->taxonomy) {
106  $taxs = $this->taxonomy->getUsageOfObject($this->parent_obj_id, true);
107  $tax_filter_options = [
108  'null' => '<b>' . $this->lng->txt('tax_filter_notax') . '</b>'
109  ];
110 
111  foreach ($taxs as $tax_entry) {
112  $tax = new ilObjTaxonomy($tax_entry['tax_id']);
113  $tax_tree = $tax->getTree();
114  $sortfield = $tax->getSortingMode() === ilObjTaxonomy::SORT_ALPHABETICAL ? 'title' : 'order_nr';
115  $children = $this->taxNodeReader($tax_tree, $sortfield, $tax_tree->readRootId());
116  $nodes = implode('-', array_map(fn($node) => $node['obj_id'], $children));
117 
118  $tax_id = $tax_entry['tax_id'] . '-0-' . $nodes;
119  $tax_title = '<b>' . $tax_entry['title'] . '</b>';
120  $tax_filter_options[$tax_id] = $tax_title;
121 
122  foreach ($children as $subtax) {
123  $stax_id = $subtax['tax_id'] . '-' . $subtax['obj_id'];
124  $stax_title = str_repeat('&nbsp; ', ($subtax['depth'] - 2) * 2)
125  . ' &boxur;&HorizontalLine; '
126  . $subtax['title'];
127 
128  $tax_filter_options[$stax_id] = $stax_title;
129  }
130  }
131  $filter_inputs['taxonomies'] = $field_factory->multiSelect($this->lng->txt("tax_filter"), $tax_filter_options);
132  }
133 
134  $active = array_fill(0, count($filter_inputs), true);
135 
136  $filter = $ui_service->filter()->standard(
137  "question_table_filter_id",
138  $action,
139  $filter_inputs,
140  $active,
141  true,
142  true
143  );
144  return $filter;
145  }
146 
147 
148  public function getColums(): array
149  {
150  $f = $this->ui_factory->table()->column();
151  $df = $this->data_factory->dateFormat();
152  $date_format = $df->withTime24($this->data_factory->dateFormat()->germanShort());
153  $icon_yes = $this->ui_factory->symbol()->icon()->custom(ilUtil::getImagePath('standard/icon_checked.svg'), 'yes');
154  $icon_no = $this->ui_factory->symbol()->icon()->custom(ilUtil::getImagePath('standard/icon_unchecked.svg'), 'no');
155 
156  $cols = [
157  'title' => $f->link($this->lng->txt('title')),
158  'description' => $f->text($this->lng->txt('description'))->withIsOptional(true, true),
159  'ttype' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true),
160  'points' => $f->number($this->lng->txt('points'))->withDecimals(2)->withIsOptional(true, true),
161  'author' => $f->text($this->lng->txt('author'))->withIsOptional(true, true),
162  'lifecycle' => $f->text($this->lng->txt('qst_lifecycle'))->withIsOptional(true, true),
163  ];
164  if ($this->taxonomy) {
165  $cols['taxonomies'] = $f->text($this->lng->txt('qpl_settings_subtab_taxonomies'))->withIsOptional(true, true);
166  }
167  $cols = array_merge($cols, [
168  'feedback' => $f->boolean($this->lng->txt('feedback'), $icon_yes, $icon_no)->withIsOptional(true, true),
169  'hints' => $f->boolean($this->lng->txt('hints'), $icon_yes, $icon_no)->withIsOptional(true, true),
170  'created' => $f->date($this->lng->txt('create_date'), $date_format)->withIsOptional(true, true),
171  'tstamp' => $f->date($this->lng->txt('last_update'), $date_format)->withIsOptional(true, true),
172  'comments' => $f->number($this->lng->txt('comments'))->withIsOptional(true, false),
173  ]);
174  return $cols;
175  }
176 
177  private function treeify(&$pointer, $stack)
178  {
179  $hop = array_shift($stack);
180  if (!$hop) {
181  return;
182  }
183  if (! array_key_exists($hop, $pointer)) {
184  $pointer[$hop] = [];
185  }
186  $this->treeify($pointer[$hop], $stack);
187  }
188 
189  private function toNestedList(array $nodes): string
190  {
191  $entries = [];
192  foreach ($nodes as $k => $n) {
193  if ($n === []) {
194  $entries[] = $k;
195  } else {
196  $entries[] = $k . $this->toNestedList($n);
197  }
198  }
199  return $this->ui_renderer->render(
200  $this->ui_factory->listing()->unordered($entries)
201  );
202  }
203 
204  private function taxNodeReader($tree, $sortfield, $node_id): array
205  {
206  $ret = [];
207  $nodes = $tree->getChildsByTypeFilter($node_id, ['taxn']);
208  usort(
209  $nodes,
210  fn($a, $b) => strcmp(
211  (string) $a[$sortfield],
212  (string) $b[$sortfield]
213  )
214  );
215 
216  foreach ($nodes as $node) {
217  $ret[] = $node;
218  foreach ($this->taxNodeReader($tree, $sortfield, $node['obj_id']) as $c) {
219  $ret[] = $c;
220  }
221  }
222  return $ret;
223  }
224 
226  int $tax_id,
227  array $stored_tax_data,
228  string $check_marker
229  ): string {
230  $tax = new ilObjTaxonomy($tax_id);
231  $tax_tree = $tax->getTree();
232  $sortfield = $tax->getSortingMode() === ilObjTaxonomy::SORT_ALPHABETICAL ? 'title' : 'order_nr';
233  $taxnodes = $this->taxNodeReader($tax_tree, $sortfield, $tax_tree->readRootId());
234 
235  $nodes = [];
236  foreach ($taxnodes as $taxnode) {
237  $taxdata = array_filter(
238  $stored_tax_data,
239  fn($data_child) => $data_child['node_id'] === $taxnode['obj_id']
240  );
241 
242  foreach (array_keys($taxdata) as $node_obj_id) {
243  $path = array_map(
244  fn($n) => in_array($n['obj_id'], array_keys($stored_tax_data)) ? $check_marker . $n['title'] : $n['title'],
245  $tax_tree->getPathFull($node_obj_id),
246  );
247  $path[0] = ilObject::_lookupTitle($tax_id);
248  $this->treeify($nodes, $path);
249  }
250  }
251  return $this->toNestedList($nodes);
252  }
253 
254  private function taxonomyRepresentation(array $taxonomy_data): string
255  {
256  $check = $this->ui_renderer->render(
257  $this->ui_factory->symbol()->icon()->custom(ilUtil::getImagePath('standard/icon_checked.svg'), 'checked')
258  );
259 
260  $taxonomies = [];
261  $taxs = $this->taxonomy->getUsageOfObject($this->parent_obj_id, true);
262  foreach ($taxs as $tax_entry) {
263  $tax_id = $tax_entry['tax_id'];
264  if (!array_key_exists($tax_id, $taxonomy_data)) {
265  continue;
266  }
267  $taxonomies[] = $this->singleTaxonomyRepresentation(
268  $tax_id,
269  $taxonomy_data[$tax_id],
270  $check
271  );
272  }
273  return implode('', $taxonomies);
274  }
275 
276  public function getRows(
277  Table\DataRowBuilder $row_builder,
278  array $visible_column_ids,
279  Range $range,
280  Order $order,
281  ?array $filter_data,
282  ?array $additional_parameters
283  ): \Generator {
284  $no_write_access = !($this->rbac->checkAccess('write', $this->request_ref_id));
285  foreach ($this->getData($order, $range) as $idx => $record) {
286  $row_id = (string) $record['question_id'];
287  $record['created'] = (new \DateTimeImmutable())->setTimestamp($record['created']);
288  $record['tstamp'] = (new \DateTimeImmutable())->setTimestamp($record['tstamp']);
289  $lifecycle = ilAssQuestionLifecycle::getInstance($record['lifecycle']);
290  $record['lifecycle'] = $lifecycle->getTranslation($this->lng);
291 
292  $title = $record['title'];
293  $to_question = $this->url_builder
294  ->withParameter($this->action_parameter_token, 'preview')
295  ->withParameter($this->row_id_token, $row_id)
296  ->buildURI()->__toString();
297  if (!(bool) $record['complete']) {
298  $title .= ' (' . $this->lng->txt('warning_question_not_complete') . ')';
299  }
300  $record['title'] = $this->ui_factory->link()->standard($title, $to_question);
301  if ($this->taxonomy) {
302  $record['taxonomies'] = $this->taxonomyRepresentation($record['taxonomies']);
303  }
304 
305  yield $row_builder->buildDataRow($row_id, $record)
306  ->withDisabledAction('move', $no_write_access)
307  ->withDisabledAction('copy', $no_write_access)
308  ->withDisabledAction('delete', $no_write_access)
309  ->withDisabledAction('feedback', $no_write_access)
310  ->withDisabledAction('hints', $no_write_access)
311  ;
312  }
313  }
314 
315  public function getTotalRowCount(
316  ?array $filter_data,
317  ?array $additional_parameters
318  ): ?int {
319  $this->setParentObjId($this->parent_obj_id);
320  $this->load();
321  return count($this->getQuestionDataArray());
322  }
323 
324  protected function getData(Order $order, Range $range): array
325  {
326  $this->setParentObjId($this->parent_obj_id);
327  $this->load();
328  $data = $this->postOrder($this->getQuestionDataArray(), $order);
329  [$offset, $length] = $range->unpack();
330  $length = $length > 0 ? $length : null;
331  return array_slice($data, $offset, $length);
332  }
333 
334  protected function getActions(): array
335  {
336  return array_merge(
337  $this->buildAction('copy', 'standard'),
338  $this->buildAction('move', 'standard'),
339  $this->buildAction('delete', 'standard'),
340  $this->buildAction('export', 'multi'),
341  $this->buildAction('preview', 'single'),
342  $this->buildAction('statistics', 'single'),
343  $this->buildAction('edit_question', 'single'),
344  $this->buildAction('edit_page', 'single'),
345  $this->buildAction('feedback', 'single'),
346  $this->buildAction('hints', 'single'),
347  $this->showCommentAction() ? $this->buildAction('comments', 'single', true) : []
348  );
349  }
350 
351  protected function buildAction(string $act, string $type, bool $async = false): array
352  {
353  $action = $this->ui_factory->table()->action()
354  ->$type(
355  $this->lng->txt($act),
356  $this->url_builder->withParameter($this->action_parameter_token, $act),
357  $this->row_id_token
358  );
359  if ($async) {
360  $action = $action->withAsync(true);
361  }
362 
363  return [$act => $action];
364  }
365 
366  protected function postOrder(array $list, \ILIAS\Data\Order $order): array
367  {
368  [$aspect, $direction] = $order->join('', function ($i, $k, $v) {
369  return [$k, $v];
370  });
371  usort($list, static function (array $a, array $b) use ($aspect): int {
372  if (is_numeric($a[$aspect]) || is_bool($a[$aspect])) {
373  return $a[$aspect] <=> $b[$aspect];
374  }
375  if (is_array($a[$aspect])) {
376  return $a[$aspect] <=> $b[$aspect];
377  }
378 
379  $aspect_a = '';
380  $aspect_b = '';
381  if ($a[$aspect] !== null) {
382  $aspect_a = $a[$aspect];
383  }
384  if ($b[$aspect] !== null) {
385  $aspect_b = $b[$aspect];
386  }
387 
388  return strcmp($aspect_a, $aspect_b);
389  });
390 
391  if ($direction === $order::DESC) {
392  $list = array_reverse($list);
393  }
394  return $list;
395  }
396 
397  private function showCommentAction(): bool
398  {
399  return $this->notes_service->domain()->commentsActive($this->parent_obj_id)
400  || $this->rbac->checkAccess('write', $this->request_ref_id);
401  }
402 }
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
taxonomyRepresentation(array $taxonomy_data)
Readable part of repository interface to ilComponentDataDB.
getData(Order $order, Range $range)
This describes a Data Table.
Definition: Data.php:31
Class ChatMainBarProvider .
static getImagePath(string $img, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
loadLanguageModule(string $a_module)
Load language module.
Filter service.
getTotalRowCount(?array $filter_data, ?array $additional_parameters)
$path
Definition: ltiservices.php:32
Both the subject and the direction need to be specified when expressing an order. ...
Definition: Order.php:12
__construct(VocabulariesInterface $vocabularies)
static _lookupTitle(int $obj_id)
$lng
getFilter(ilUIService $ui_service, string $action)
Filters should be part of the Table; for now, since they are not fully integrated, they are rendered and applied seperately.
singleTaxonomyRepresentation(int $tax_id, array $stored_tax_data, string $check_marker)
$lifecycle
getRows(Table\DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, ?array $filter_data, ?array $additional_parameters)
buildAction(string $act, string $type, bool $async=false)
toNestedList(array $nodes)
__construct(protected UIFactory $ui_factory, protected UIRenderer $ui_renderer, protected DataFactory $data_factory, protected Refinery $refinery, protected URLBuilder $url_builder, protected URLBuilderToken $action_parameter_token, protected URLBuilderToken $row_id_token, protected ilDBInterface $db, protected ilLanguage $lng, protected ilComponentRepository $component_repository, protected ilRbacSystem $rbac, protected ?TaxonomyService $taxonomy, protected NotesService $notes_service, protected int $parent_obj_id, protected int $request_ref_id)
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
$check
Definition: buildRTE.php:81
treeify(&$pointer, $stack)
URLBuilder.
Definition: URLBuilder.php:39
A simple class to express a range of whole positive numbers.
Definition: Range.php:30
postOrder(array $list, \ILIAS\Data\Order $order)
taxNodeReader($tree, $sortfield, $node_id)
static _getQuestionTypes($all_tags=false, $fixOrder=false, $withDeprecatedTypes=true)
setAvailableTaxonomyIds($availableTaxonomyIds)
Refinery Factory $refinery