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