ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
QuestionTable.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
23use ILIAS\UI\Factory as UIFactory;
24use ILIAS\UI\Renderer as UIRenderer;
25use ILIAS\Data\Factory as DataFactory;
33use ILIAS\Taxonomy\DomainService as TaxonomyService;
34use ILIAS\Notes\Service as NotesService;
35
36class 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 if ($children === []) {
121 continue;
122 }
123 $tax_filter_options[$tax_id] = $tax_title;
124
125 foreach ($children as $subtax) {
126 $stax_id = $subtax['tax_id'] . '-' . $subtax['obj_id'];
127 $stax_title = str_repeat('&nbsp; ', ($subtax['depth'] - 2) * 2)
128 . ' &boxur;&HorizontalLine; '
129 . $subtax['title'];
130
131 $tax_filter_options[$stax_id] = $stax_title;
132 }
133 }
134 $filter_inputs['taxonomies'] = $field_factory->multiSelect($this->lng->txt("tax_filter"), $tax_filter_options);
135
136 $active = array_fill(0, count($filter_inputs), true);
137
138 $filter = $ui_service->filter()->standard(
139 'question_table_filter_id',
140 $action,
141 $filter_inputs,
142 $active,
143 true,
144 true
145 );
146 return $filter;
147 }
148
149
150 public function getColums(): array
151 {
152 $f = $this->ui_factory->table()->column();
153 $df = $this->data_factory->dateFormat();
154 $icon_yes = $this->ui_factory->symbol()->icon()->custom(\ilUtil::getImagePath('standard/icon_checked.svg'), 'yes');
155 $icon_no = $this->ui_factory->symbol()->icon()->custom(\ilUtil::getImagePath('standard/icon_unchecked.svg'), 'no');
156
157 return [
158 'title' => $f->link($this->lng->txt('title')),
159 'description' => $f->text($this->lng->txt('description'))->withIsOptional(true, true),
160 'ttype' => $f->text($this->lng->txt('question_type'))->withIsOptional(true, true),
161 'points' => $f->number($this->lng->txt('points'))->withDecimals(2)->withIsOptional(true, true),
162 'author' => $f->text($this->lng->txt('author'))->withIsOptional(true, true),
163 'lifecycle' => $f->text($this->lng->txt('qst_lifecycle'))->withIsOptional(true, true),
164 'taxonomies' => $f->text($this->lng->txt('qpl_settings_subtab_taxonomies'))->withIsOptional(true, true),
165 'feedback' => $f->boolean($this->lng->txt('feedback'), $icon_yes, $icon_no)->withIsOptional(true, true),
166 'created' => $f->date(
167 $this->lng->txt('create_date'),
168 $this->current_user->getDateTimeFormat()
169 )->withIsOptional(true, true),
170 'tstamp' => $f->date(
171 $this->lng->txt('last_update'),
172 $this->current_user->getDateTimeFormat()
173 )->withIsOptional(true, true),
174 'comments' => $f->number($this->lng->txt('comments'))->withIsOptional(true, false),
175 ];
176 }
177
178 private function treeify(&$pointer, $stack)
179 {
180 $hop = array_shift($stack);
181 if (!$hop) {
182 return;
183 }
184 if (! array_key_exists($hop, $pointer)) {
185 $pointer[$hop] = [];
186 }
187 $this->treeify($pointer[$hop], $stack);
188 }
189
190 private function toNestedList(array $nodes): string
191 {
192 $entries = [];
193 foreach ($nodes as $k => $n) {
194 if ($n === []) {
195 $entries[] = $k;
196 } else {
197 $entries[] = $k . $this->toNestedList($n);
198 }
199 }
200 return $this->ui_renderer->render(
201 $this->ui_factory->listing()->unordered($entries)
202 );
203 }
204
205 private function taxNodeReader($tree, $sortfield, $node_id): array
206 {
207 $ret = [];
208 $nodes = $tree->getChildsByTypeFilter($node_id, ['taxn']);
209 usort(
210 $nodes,
211 fn($a, $b) => strcmp(
212 (string) $a[$sortfield],
213 (string) $b[$sortfield]
214 )
215 );
216
217 foreach ($nodes as $node) {
218 $ret[] = $node;
219 foreach ($this->taxNodeReader($tree, $sortfield, $node['obj_id']) as $c) {
220 $ret[] = $c;
221 }
222 }
223 return $ret;
224 }
225
227 int $tax_id,
228 array $stored_tax_data,
229 string $check_marker
230 ): string {
231 $tax = new \ilObjTaxonomy($tax_id);
232 $tax_tree = $tax->getTree();
233 $sortfield = $tax->getSortingMode() === \ilObjTaxonomy::SORT_ALPHABETICAL ? 'title' : 'order_nr';
234 $taxnodes = $this->taxNodeReader($tax_tree, $sortfield, $tax_tree->readRootId());
235
236 $nodes = [];
237 foreach ($taxnodes as $taxnode) {
238 $taxdata = array_filter(
239 $stored_tax_data,
240 fn($data_child) => $data_child['node_id'] === $taxnode['obj_id']
241 );
242
243 foreach (array_keys($taxdata) as $node_obj_id) {
244 $path = array_map(
245 fn($n) => in_array($n['obj_id'], array_keys($stored_tax_data)) ? $check_marker . $n['title'] : $n['title'],
246 $tax_tree->getPathFull($node_obj_id),
247 );
248 $path[0] = \ilObject::_lookupTitle($tax_id);
249 $this->treeify($nodes, $path);
250 }
251 }
252 return $this->toNestedList($nodes);
253 }
254
255 private function taxonomyRepresentation(array $taxonomy_data): string
256 {
257 $check = $this->ui_renderer->render(
258 $this->ui_factory->symbol()->icon()->custom(\ilUtil::getImagePath('standard/icon_checked.svg'), 'checked')
259 );
260
261 $taxonomies = [];
262 $taxs = $this->taxonomy->getUsageOfObject($this->parent_obj_id, true);
263 foreach ($taxs as $tax_entry) {
264 $tax_id = $tax_entry['tax_id'];
265 if (!array_key_exists($tax_id, $taxonomy_data)) {
266 continue;
267 }
268 $taxonomies[] = $this->singleTaxonomyRepresentation(
269 $tax_id,
270 $taxonomy_data[$tax_id],
271 $check
272 );
273 }
274 return implode('', $taxonomies);
275 }
276
277 public function getRows(
278 Table\DataRowBuilder $row_builder,
279 array $visible_column_ids,
281 Order $order,
282 ?array $filter_data,
283 ?array $additional_parameters
284 ): \Generator {
285 $timezone = new \DateTimeZone($this->current_user->getTimeZone());
286 foreach ($this->getData($order, $range) as $record) {
287 $row_id = (string) $record['question_id'];
288 $record['created'] = (new \DateTimeImmutable("@{$record['created']}"))->setTimezone($timezone);
289 $record['tstamp'] = (new \DateTimeImmutable("@{$record['tstamp']}"))->setTimezone($timezone);
290 $lifecycle = \ilAssQuestionLifecycle::getInstance($record['lifecycle']);
291 $record['lifecycle'] = $lifecycle->getTranslation($this->lng);
292
293 $title = $record['title'];
294 $to_question = $this->url_builder
295 ->withParameter($this->action_parameter_token, 'preview')
296 ->withParameter($this->row_id_token, $row_id)
297 ->buildURI()->__toString();
298 if (!(bool) $record['complete']) {
299 $title .= ' (' . $this->lng->txt('warning_question_not_complete') . ')';
300 }
301 $record['title'] = $this->ui_factory->link()->standard($title, $to_question);
302 $record['taxonomies'] = $this->taxonomyRepresentation($record['taxonomies']);
303
304 yield $row_builder->buildDataRow($row_id, $record);
305 }
306 }
307
308 public function getTotalRowCount(
309 ?array $filter_data,
310 ?array $additional_parameters
311 ): ?int {
312 $this->setParentObjId($this->parent_obj_id);
313 $this->load();
314 return count($this->getQuestionDataArray());
315 }
316
317 protected function getData(Order $order, Range $range): array
318 {
319 $this->setParentObjId($this->parent_obj_id);
320 $this->load();
321 $data = $this->postOrder($this->getQuestionDataArray(), $order);
322 [$offset, $length] = $range->unpack();
323 $length = $length > 0 ? $length : null;
324 return array_slice($data, $offset, $length);
325 }
326
327 protected function getActions(): array
328 {
329 $write_access = $this->rbac->checkAccess('write', $this->request_ref_id);
330 return array_merge(
331 $this->buildAction('copy', 'standard'),
332 $write_access ? $this->buildAction('move', 'standard') : [],
333 $write_access ? $this->buildAction('delete', 'standard') : [],
334 $this->buildAction('export', 'multi'),
335 $this->buildAction('preview', 'single'),
336 $this->buildAction('statistics', 'single'),
337 $write_access ? $this->buildAction('edit_question', 'single') : [],
338 $write_access ? $this->buildAction('edit_page', 'single') : [],
339 $write_access ? $this->buildAction('feedback', 'single') : [],
340 $write_access ? $this->buildAction(\ilBulkEditQuestionsGUI::CMD_EDITTAUTHOR, 'multi') : [],
341 $write_access ? $this->buildAction(\ilBulkEditQuestionsGUI::CMD_EDITLIFECYCLE, 'multi') : [],
342 $write_access ? $this->buildAction(\ilBulkEditQuestionsGUI::CMD_EDITTAXONOMIES, 'multi') : [],
343 $this->showCommentAction() ? $this->buildAction('comments', 'single', true) : []
344 );
345 }
346
347 protected function buildAction(string $act, string $type, bool $async = false): array
348 {
349 $action = $this->ui_factory->table()->action()
350 ->$type(
351 $this->lng->txt($act),
352 $this->url_builder->withParameter($this->action_parameter_token, $act),
353 $this->row_id_token
354 );
355 if ($async) {
356 $action = $action->withAsync(true);
357 }
358
359 return [$act => $action];
360 }
361
362 protected function postOrder(array $list, \ILIAS\Data\Order $order): array
363 {
364 [$aspect, $direction] = $order->join('', function ($i, $k, $v) {
365 return [$k, $v];
366 });
367 usort($list, static function (array $a, array $b) use ($aspect): int {
368 if (is_numeric($a[$aspect]) || is_bool($a[$aspect])) {
369 return $a[$aspect] <=> $b[$aspect];
370 }
371 if (is_array($a[$aspect])) {
372 return $a[$aspect] <=> $b[$aspect];
373 }
374
375 $aspect_a = '';
376 $aspect_b = '';
377 if ($a[$aspect] !== null) {
378 $aspect_a = $a[$aspect];
379 }
380 if ($b[$aspect] !== null) {
381 $aspect_b = $b[$aspect];
382 }
383
384 return strcoll($aspect_a, $aspect_b);
385 });
386
387 if ($direction === $order::DESC) {
388 $list = array_reverse($list);
389 }
390 return $list;
391 }
392
393 private function showCommentAction(): bool
394 {
395 return $this->notes_service->domain()->commentsActive($this->parent_obj_id)
396 || $this->rbac->checkAccess('write', $this->request_ref_id);
397 }
398}
$lifecycle
$check
Definition: buildRTE.php:81
Builds a Color from either hex- or rgb values.
Definition: Factory.php:31
Builds data types.
Definition: Factory.php:36
Both the subject and the direction need to be specified when expressing an order.
Definition: Order.php:29
A simple class to express a naive range of whole positive numbers.
Definition: Range.php:29
buildAction(string $act, string $type, bool $async=false)
getRows(Table\DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, ?array $filter_data, ?array $additional_parameters)
__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)
singleTaxonomyRepresentation(int $tax_id, array $stored_tax_data, string $check_marker)
getFilter(\ilUIService $ui_service, string $action)
Filters should be part of the Table; for now, since they are not fully integrated,...
postOrder(array $list, \ILIAS\Data\Order $order)
getTotalRowCount(?array $filter_data, ?array $additional_parameters)
setAvailableTaxonomyIds(array $availableTaxonomyIds)
language handling
static _getQuestionTypes($all_tags=false, $fixOrder=false, $withDeprecatedTypes=true)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
User class.
static _lookupTitle(int $obj_id)
class ilRbacSystem system function like checkAccess, addActiveRole ... Supporting system functions ar...
Filter service.
static getImagePath(string $image_name, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
$c
Definition: deliver.php:25
This describes a standard filter.
Definition: Standard.php:27
This describes a Data Table.
Definition: Data.php:31
An entity that renders components to a string output.
Definition: Renderer.php:31
Readable part of repository interface to ilComponentDataDB.
Interface ilDBInterface.
$path
Definition: ltiservices.php:30
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
global $lng
Definition: privfeed.php:31