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