xref: /plugin/struct/meta/AggregationTable.php (revision b360e3b5f0dd65e1a784310a1ef841ecb24beeab)
1<?php
2
3namespace dokuwiki\plugin\struct\meta;
4
5/**
6 * Creates the table aggregation output
7 *
8 * @package dokuwiki\plugin\struct\meta
9 */
10class AggregationTable
11{
12    /**
13     * @var string the page id of the page this is rendered to
14     */
15    protected $id;
16    /**
17     * @var string the Type of renderer used
18     */
19    protected $mode;
20    /**
21     * @var \Doku_Renderer the DokuWiki renderer used to create the output
22     */
23    protected $renderer;
24    /**
25     * @var SearchConfig the configured search - gives access to columns etc.
26     */
27    protected $searchConfig;
28
29    /**
30     * @var Column[] the list of columns to be displayed
31     */
32    protected $columns;
33
34    /**
35     * @var  Value[][] the search result
36     */
37    protected $result;
38
39    /**
40     * @var int number of all results
41     */
42    protected $resultCount;
43
44    /**
45     * @var string[] the result PIDs for each row
46     */
47    protected $resultPIDs;
48    protected $resultRids;
49    protected $resultRevs;
50
51    /**
52     * @var array for summing up columns
53     */
54    protected $sums;
55
56    /**
57     * @var bool skip full table when no results found
58     */
59    protected $simplenone = true;
60
61    /**
62     * @todo we might be able to get rid of this helper and move this to SearchConfig
63     * @var \helper_plugin_struct_config
64     */
65    protected $helper;
66
67    /**
68     * @var array the original configuration data
69     */
70    protected $data;
71
72    /**
73     * Initialize the Aggregation renderer and executes the search
74     *
75     * You need to call @param string $id
76     * @param string $mode
77     * @param \Doku_Renderer $renderer
78     * @param SearchConfig $searchConfig
79     * @see render() on the resulting object.
80     *
81     */
82    public function __construct($id, $mode, \Doku_Renderer $renderer, SearchConfig $searchConfig)
83    {
84        $this->id = $id;
85        $this->mode = $mode;
86        $this->renderer = $renderer;
87        $this->searchConfig = $searchConfig;
88        $this->data = $searchConfig->getConf();
89        $this->columns = $searchConfig->getColumns();
90
91        $this->result = $this->searchConfig->execute();
92        $this->resultCount = $this->searchConfig->getCount();
93        $this->resultPIDs = $this->searchConfig->getPids();
94        $this->resultRids = $this->searchConfig->getRids();
95        $this->resultRevs = $this->searchConfig->getRevs();
96        $this->helper = plugin_load('helper', 'struct_config');
97    }
98
99    /**
100     * Returns the page id for the table
101     */
102    public function getID()
103    {
104        return $this->id;
105    }
106
107    /**
108     * Create the table on the renderer
109     */
110    public function render()
111    {
112
113        // abort early if there are no results at all (not filtered)
114        if (!$this->resultCount && !$this->isDynamicallyFiltered() && $this->simplenone) {
115            $this->startScope();
116            $this->renderer->cdata($this->helper->getLang('none'));
117            $this->finishScope();
118            return;
119        }
120
121        $this->startScope();
122        $this->renderActiveFilters();
123
124        $rendercontext = array(
125            'table' => $this,
126            'renderer' => $this->renderer,
127            'format' => $this->mode,
128            'search' => $this->searchConfig,
129            'columns' => $this->columns,
130            'data' => $this->result
131        );
132
133        $event = new \Doku_Event(
134            'PLUGIN_STRUCT_RENDER_AGGREGATION_TABLE',
135            $rendercontext
136        );
137        $event->trigger([$this, 'renderTable']);
138
139        // export handle
140        $this->renderExportControls();
141        $this->finishScope();
142    }
143
144    /**
145     * Render the default aggregation table
146     */
147    public function renderTable($rendercontext)
148    {
149        $this->renderer->table_open();
150
151        // header
152        $this->renderer->tablethead_open();
153        $this->renderColumnHeaders();
154        $this->renderDynamicFilters();
155        $this->renderer->tablethead_close();
156
157        if ($this->resultCount) {
158            // actual data
159            $this->renderer->tabletbody_open();
160            $this->renderResult();
161            $this->renderer->tabletbody_close();
162
163            // footer (tfoot is develonly currently)
164            if (method_exists($this->renderer, 'tabletfoot_open')) $this->renderer->tabletfoot_open();
165            $this->renderSums();
166            $this->renderPagingControls();
167            if (method_exists($this->renderer, 'tabletfoot_close')) $this->renderer->tabletfoot_close();
168        } else {
169            // nothing found
170            $this->renderEmptyResult();
171        }
172
173        // table close
174        $this->renderer->table_close();
175    }
176
177    /**
178     * Adds additional info to document and renderer in XHTML mode
179     *
180     * @see finishScope()
181     */
182    protected function startScope()
183    {
184        // unique identifier for this aggregation
185        $this->renderer->info['struct_table_hash'] = md5(var_export($this->data, true));
186
187        // wrapping div
188        if ($this->mode != 'xhtml') return;
189        $this->renderer->doc .= "<div class=\"structaggregation\">";
190    }
191
192    /**
193     * Closes the table and anything opened in startScope()
194     *
195     * @see startScope()
196     */
197    protected function finishScope()
198    {
199        // remove identifier from renderer again
200        if (isset($this->renderer->info['struct_table_hash'])) {
201            unset($this->renderer->info['struct_table_hash']);
202        }
203
204        // wrapping div
205        if ($this->mode != 'xhtml') return;
206        $this->renderer->doc .= '</div>';
207    }
208
209    /**
210     * Displays info about the currently applied filters
211     */
212    protected function renderActiveFilters()
213    {
214        if ($this->mode != 'xhtml') return;
215        $dynamic = $this->searchConfig->getDynamicParameters();
216        $filters = $dynamic->getFilters();
217        if (!$filters) return;
218
219        $fltrs = array();
220        foreach ($filters as $column => $filter) {
221            list($comp, $value) = $filter;
222
223            // display the filters in a human readable format
224            foreach ($this->columns as $col) {
225                if ($column === $col->getFullQualifiedLabel()) {
226                    $column = $col->getTranslatedLabel();
227                }
228            }
229            $fltrs[] = sprintf('"%s" %s "%s"', $column, $this->helper->getLang("comparator $comp"), $value);
230        }
231
232        $this->renderer->doc .= '<div class="filter">';
233        $this->renderer->doc .= '<h4>' .
234            sprintf(
235                $this->helper->getLang('tablefilteredby'),
236                hsc(implode(' & ', $fltrs))
237            ) .
238            '</h4>';
239        $this->renderer->doc .= '<div class="resetfilter">';
240        $this->renderer->internallink($this->id, $this->helper->getLang('tableresetfilter'));
241        $this->renderer->doc .= '</div>';
242        $this->renderer->doc .= '</div>';
243    }
244
245    /**
246     * Shows the column headers with links to sort by column
247     */
248    protected function renderColumnHeaders()
249    {
250        $this->renderer->tablerow_open();
251
252        // additional column for row numbers
253        if (!empty($this->data['rownumbers'])) {
254            $this->renderer->tableheader_open();
255            $this->renderer->cdata('#');
256            $this->renderer->tableheader_close();
257        }
258
259        // show all headers
260        foreach ($this->columns as $num => $column) {
261            $header = '';
262            if (isset($this->data['headers'][$num])) {
263                $header = $this->data['headers'][$num];
264            }
265
266            // use field label if no header was set
267            if (blank($header)) {
268                if (is_a($column, 'dokuwiki\plugin\struct\meta\Column')) {
269                    $header = $column->getTranslatedLabel();
270                } else {
271                    $header = 'column ' . $num; // this should never happen
272                }
273            }
274
275            // simple mode first
276            if ($this->mode != 'xhtml') {
277                $this->renderer->tableheader_open();
278                $this->renderer->cdata($header);
279                $this->renderer->tableheader_close();
280                continue;
281            }
282
283            // still here? create custom header for more flexibility
284
285            // width setting, widths are prevalidated, no escape needed
286            $width = '';
287            if (isset($this->data['widths'][$num]) && $this->data['widths'][$num] != '-') {
288                $width = ' style="min-width: ' . $this->data['widths'][$num] . ';' .
289                    'max-width: ' . $this->data['widths'][$num] . ';"';
290            }
291
292            // prepare data attribute for inline edits
293            if (
294                !is_a($column, '\dokuwiki\plugin\struct\meta\PageColumn') &&
295                !is_a($column, '\dokuwiki\plugin\struct\meta\RevisionColumn')
296            ) {
297                $data = 'data-field="' . hsc($column->getFullQualifiedLabel()) . '"';
298            } else {
299                $data = '';
300            }
301
302            // sort indicator and link
303            $sortclass = '';
304            $sorts = $this->searchConfig->getSorts();
305            $dynamic = $this->searchConfig->getDynamicParameters();
306            $dynamic->setSort($column, true);
307            if (isset($sorts[$column->getFullQualifiedLabel()])) {
308                list(/*colname*/, $currentSort) = $sorts[$column->getFullQualifiedLabel()];
309                if ($currentSort) {
310                    $sortclass = 'sort-down';
311                    $dynamic->setSort($column, false);
312                } else {
313                    $sortclass = 'sort-up';
314                }
315            }
316            $link = wl($this->id, $dynamic->getURLParameters());
317
318            // output XHTML header
319            $this->renderer->doc .= "<th $width $data>";
320            $this->renderer->doc .= '<a href="' . $link . '" class="' . $sortclass . '" ' .
321                'title="' . $this->helper->getLang('sort') . '">' . hsc($header) . '</a>';
322            $this->renderer->doc .= '</th>';
323        }
324
325        $this->renderer->tablerow_close();
326    }
327
328    /**
329     * Is the result set currently dynamically filtered?
330     * @return bool
331     */
332    protected function isDynamicallyFiltered()
333    {
334        if ($this->mode != 'xhtml') return false;
335        if (!$this->data['dynfilters']) return false;
336
337        $dynamic = $this->searchConfig->getDynamicParameters();
338        return (bool)$dynamic->getFilters();
339    }
340
341    /**
342     * Add input fields for dynamic filtering
343     */
344    protected function renderDynamicFilters()
345    {
346        if ($this->mode != 'xhtml') return;
347        if (empty($this->data['dynfilters'])) return;
348        if (is_a($this->renderer, 'renderer_plugin_dw2pdf')) {
349            return;
350        }
351        global $conf;
352
353        $this->renderer->doc .= '<tr class="dataflt">';
354
355        // add extra column for row numbers
356        if ($this->data['rownumbers']) {
357            $this->renderer->doc .= '<th></th>';
358        }
359
360        // each column gets a form
361        foreach ($this->columns as $column) {
362            $this->renderer->doc .= '<th>';
363
364            // BEGIN FORM
365            $form = new \Doku_Form(array('method' => 'GET', 'action' => wl($this->id)));
366            unset($form->_hidden['sectok']); // we don't need it here
367            if (!$conf['userewrite']) $form->addHidden('id', $this->id);
368
369            // current value
370            $dynamic = $this->searchConfig->getDynamicParameters();
371            $filters = $dynamic->getFilters();
372            if (isset($filters[$column->getFullQualifiedLabel()])) {
373                list(, $current) = $filters[$column->getFullQualifiedLabel()];
374                $dynamic->removeFilter($column);
375            } else {
376                $current = '';
377            }
378
379            // Add current request params
380            $params = $dynamic->getURLParameters();
381            foreach ($params as $key => $val) {
382                $form->addHidden($key, $val);
383            }
384
385            // add input field
386            $key = $column->getFullQualifiedLabel() . $column->getType()->getDefaultComparator();
387            $form->addElement(
388                form_makeField('text', SearchConfigParameters::$PARAM_FILTER . '[' . $key . ']', $current, '')
389            );
390            $this->renderer->doc .= $form->getForm();
391            // END FORM
392
393            $this->renderer->doc .= '</th>';
394        }
395        $this->renderer->doc .= '</tr>';
396    }
397
398    /**
399     * Display the actual table data
400     */
401    protected function renderResult()
402    {
403        foreach ($this->result as $rownum => $row) {
404            $data = array(
405                'id' => $this->id,
406                'mode' => $this->mode,
407                'renderer' => $this->renderer,
408                'searchConfig' => $this->searchConfig,
409                'data' => $this->data,
410                'rownum' => &$rownum,
411                'row' => &$row,
412            );
413            $evt = new \Doku_Event('PLUGIN_STRUCT_AGGREGATIONTABLE_RENDERRESULTROW', $data);
414            if ($evt->advise_before()) {
415                $this->renderResultRow($rownum, $row);
416            }
417            $evt->advise_after();
418        }
419    }
420
421    /**
422     * Render a single result row
423     *
424     * @param int $rownum
425     * @param array $row
426     */
427    protected function renderResultRow($rownum, $row)
428    {
429        $this->renderer->tablerow_open();
430
431        // add data attribute for inline edit
432        if ($this->mode == 'xhtml') {
433            $pid = $this->resultPIDs[$rownum];
434            $rid = $this->resultRids[$rownum];
435            $rev = $this->resultRevs[$rownum];
436            $this->renderer->doc = substr(rtrim($this->renderer->doc), 0, -1); // remove closing '>'
437            $this->renderer->doc .= ' data-pid="' . hsc($pid) . '" data-rev="' . $rev . '" data-rid="' . $rid . '">';
438        }
439
440        // row number column
441        if (!empty($this->data['rownumbers'])) {
442            $this->renderer->tablecell_open();
443            $searchConfigConf = $this->searchConfig->getConf();
444            $this->renderer->cdata($rownum + $searchConfigConf['offset'] + 1);
445            $this->renderer->tablecell_close();
446        }
447
448        /** @var Value $value */
449        foreach ($row as $colnum => $value) {
450            $align = isset($this->data['align'][$colnum]) ? $this->data['align'][$colnum] : null;
451            $this->renderer->tablecell_open(1, $align);
452            $value->render($this->renderer, $this->mode);
453            $this->renderer->tablecell_close();
454
455            // summarize
456            if (!empty($this->data['summarize']) && is_numeric($value->getValue())) {
457                if (!isset($this->sums[$colnum])) {
458                    $this->sums[$colnum] = 0;
459                }
460                $this->sums[$colnum] += $value->getValue();
461            }
462        }
463        $this->renderer->tablerow_close();
464    }
465
466    /**
467     * Renders an information row for when no results were found
468     */
469    protected function renderEmptyResult()
470    {
471        $this->renderer->tablerow_open();
472        $this->renderer->tablecell_open(count($this->columns) + $this->data['rownumbers'], 'center');
473        $this->renderer->cdata($this->helper->getLang('none'));
474        $this->renderer->tablecell_close();
475        $this->renderer->tablerow_close();
476    }
477
478    /**
479     * Add sums if wanted
480     */
481    protected function renderSums()
482    {
483        if (empty($this->data['summarize'])) return;
484
485        $this->renderer->info['struct_table_meta'] = true;
486        if ($this->mode == 'xhtml') {
487            /** @noinspection PhpMethodParametersCountMismatchInspection */
488            $this->renderer->tablerow_open('summarize');
489        } else {
490            $this->renderer->tablerow_open();
491        }
492
493        if ($this->data['rownumbers']) {
494            $this->renderer->tableheader_open();
495            $this->renderer->tableheader_close();
496        }
497
498        $len = count($this->columns);
499        for ($i = 0; $i < $len; $i++) {
500            $this->renderer->tableheader_open(1, $this->data['align'][$i]);
501            if (!empty($this->sums[$i])) {
502                $this->renderer->cdata('∑ ');
503                $this->columns[$i]->getType()->renderValue($this->sums[$i], $this->renderer, $this->mode);
504            } else {
505                if ($this->mode == 'xhtml') {
506                    $this->renderer->doc .= '&nbsp;';
507                }
508            }
509            $this->renderer->tableheader_close();
510        }
511        $this->renderer->tablerow_close();
512        $this->renderer->info['struct_table_meta'] = false;
513    }
514
515    /**
516     * Adds paging controls to the table
517     */
518    protected function renderPagingControls()
519    {
520        if (empty($this->data['limit'])) return;
521        if ($this->mode != 'xhtml') return;
522
523        $this->renderer->info['struct_table_meta'] = true;
524        $this->renderer->tablerow_open();
525        $this->renderer->tableheader_open((count($this->columns) + ($this->data['rownumbers'] ? 1 : 0)));
526        $offset = $this->data['offset'];
527
528        // prev link
529        if ($offset) {
530            $prev = $offset - $this->data['limit'];
531            if ($prev < 0) {
532                $prev = 0;
533            }
534
535            $dynamic = $this->searchConfig->getDynamicParameters();
536            $dynamic->setOffset($prev);
537            $link = wl($this->id, $dynamic->getURLParameters());
538            $this->renderer->doc .= '<a href="' . $link . '" class="prev">' . $this->helper->getLang('prev') . '</a>';
539        }
540
541        // next link
542        if ($this->resultCount > $offset + $this->data['limit']) {
543            $next = $offset + $this->data['limit'];
544            $dynamic = $this->searchConfig->getDynamicParameters();
545            $dynamic->setOffset($next);
546            $link = wl($this->id, $dynamic->getURLParameters());
547            $this->renderer->doc .= '<a href="' . $link . '" class="next">' . $this->helper->getLang('next') . '</a>';
548        }
549
550        $this->renderer->tableheader_close();
551        $this->renderer->tablerow_close();
552        $this->renderer->info['struct_table_meta'] = true;
553    }
554
555    /**
556     * Adds CSV export controls
557     */
558    protected function renderExportControls()
559    {
560        if ($this->mode != 'xhtml') return;
561        if (empty($this->data['csv'])) return;
562        if (!$this->resultCount) return;
563
564        $dynamic = $this->searchConfig->getDynamicParameters();
565        $params = $dynamic->getURLParameters();
566        $params['hash'] = $this->renderer->info['struct_table_hash'];
567
568        // FIXME apply dynamic filters
569        $link = exportlink($this->id, 'struct_csv', $params);
570
571        $this->renderer->doc .= '<a href="' . $link . '" class="export mediafile mf_csv">' .
572            $this->helper->getLang('csvexport') . '</a>';
573    }
574}
575